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/ .bundle/
log/*.log log/*.log
pkg/ pkg/
test/dummy/db/*.sqlite3 test/dummy/db/*.sqlite3
test/dummy/db/*.sqlite3-journal test/dummy/db/*.sqlite3-journal
test/dummy/log/*.log test/dummy/log/*.log
test/dummy/tmp/ test/dummy/tmp/
test/dummy/.sass-cache test/dummy/.sass-cache

28
Gemfile Normal file → Executable file
View File

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

40
MIT-LICENSE Normal file → Executable file
View File

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

68
Rakefile Normal file → Executable file
View File

@ -1,34 +1,34 @@
begin begin
require 'bundler/setup' require 'bundler/setup'
rescue LoadError rescue LoadError
puts 'You must `gem install bundler` and `bundle install` to run rake tasks' puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
end end
require 'rdoc/task' require 'rdoc/task'
RDoc::Task.new(:rdoc) do |rdoc| RDoc::Task.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc' rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'UniversalTable' rdoc.title = 'UniversalTable'
rdoc.options << '--line-numbers' rdoc.options << '--line-numbers'
rdoc.rdoc_files.include('README.rdoc') rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb') rdoc.rdoc_files.include('lib/**/*.rb')
end end
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
load 'rails/tasks/engine.rake' load 'rails/tasks/engine.rake'
Bundler::GemHelper.install_tasks Bundler::GemHelper.install_tasks
require 'rake/testtask' require 'rake/testtask'
Rake::TestTask.new(:test) do |t| Rake::TestTask.new(:test) do |t|
t.libs << 'lib' t.libs << 'lib'
t.libs << 'test' t.libs << 'test'
t.pattern = 'test/**/*_test.rb' t.pattern = 'test/**/*_test.rb'
t.verbose = false t.verbose = false
end end
task default: :test 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 __version__ = '1.0.0'
export const __author__ = 'author' export const __author__ = 'author'
if (typeof String.prototype.startsWith != 'function') { if (typeof String.prototype.startsWith != 'function') {
String.prototype.startsWith = function (p) { String.prototype.startsWith = function (p) {
return this.slice(0, p.length) === p return this.slice(0, p.length) === p
} }
} }
export const Direction = { export const Direction = {
left: -1, left: -1,
center: 0, center: 0,
right: 1, right: 1,
of: function (dir) { of: function (dir) {
if (!dir || dir === -1 || dir === 0 || dir === 1) { if (!dir || dir === -1 || dir === 0 || dir === 1) {
return dir return dir
} }
if (dir === '-1' || dir === '0' || dir === '1') { if (dir === '-1' || dir === '0' || dir === '1') {
return parseInt(dir) return parseInt(dir)
} }
if (dir.toLowerCase() === 'left') { if (dir.toLowerCase() === 'left') {
return this.left return this.left
} }
if (dir.toLowerCase() === 'right') { if (dir.toLowerCase() === 'right') {
return this.right return this.right
} }
if (dir.toLowerCase() === 'center') { if (dir.toLowerCase() === 'center') {
return this.center return this.center
} }
}, },
} }
export const EventType = { show: 1, resize: 2, edit: 3, select: 4 } 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 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 } export const LogLevel = { debug: 1, info: 2, warn: 3, error: 4, disable: 9 }
// an noop function define // an noop function define
var _noop = function () {} var _noop = function () {}
export let logger = export let logger =
typeof console === 'undefined' typeof console === 'undefined'
? { ? {
level: _noop, level: _noop,
log: _noop, log: _noop,
debug: _noop, debug: _noop,
info: _noop, info: _noop,
warn: _noop, warn: _noop,
error: _noop, error: _noop,
} }
: { : {
level: setup_logger_level, level: setup_logger_level,
log: console.log, log: console.log,
debug: console.debug, debug: console.debug,
info: console.info, info: console.info,
warn: console.warn, warn: console.warn,
error: console.error, error: console.error,
} }
function setup_logger_level(log_level) { function setup_logger_level(log_level) {
if (log_level > LogLevel.debug) { if (log_level > LogLevel.debug) {
logger.debug = _noop logger.debug = _noop
} else { } else {
logger.debug = console.debug logger.debug = console.debug
} }
if (log_level > LogLevel.info) { if (log_level > LogLevel.info) {
logger.info = _noop logger.info = _noop
} else { } else {
logger.info = console.info logger.info = console.info
} }
if (log_level > LogLevel.warn) { if (log_level > LogLevel.warn) {
logger.warn = _noop logger.warn = _noop
} else { } else {
logger.warn = console.warn logger.warn = console.warn
} }
if (log_level > LogLevel.error) { if (log_level > LogLevel.error) {
logger.error = _noop logger.error = _noop
} else { } else {
logger.error = console.error 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 */ /* important section */
.jsmind-inner { .jsmind-inner {
position: relative; position: relative;
overflow: auto; overflow: auto;
width: 100%; width: 100%;
height: 100%; height: 100%;
outline: none; outline: none;
} /*box-shadow:0 0 2px #000;*/ } /*box-shadow:0 0 2px #000;*/
.jsmind-inner { .jsmind-inner {
moz-user-select: -moz-none; moz-user-select: -moz-none;
-moz-user-select: none; -moz-user-select: none;
-o-user-select: none; -o-user-select: none;
-khtml-user-select: none; -khtml-user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
} }
.jsmind-inner canvas { .jsmind-inner canvas {
position: absolute; position: absolute;
} }
/* z-index:1 */ /* z-index:1 */
svg.jsmind { svg.jsmind {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
} }
canvas.jsmind { canvas.jsmind {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
} }
/* z-index:2 */ /* z-index:2 */
jmnodes { jmnodes {
position: absolute; position: absolute;
z-index: 2; z-index: 2;
background-color: rgba(0, 0, 0, 0); background-color: rgba(0, 0, 0, 0);
} /*background color is necessary*/ } /*background color is necessary*/
jmnode { jmnode {
position: absolute; position: absolute;
cursor: default; cursor: default;
max-width: 400px; max-width: 400px;
} }
jmexpander { jmexpander {
position: absolute; position: absolute;
width: 11px; width: 11px;
height: 11px; height: 11px;
display: block; display: block;
overflow: hidden; overflow: hidden;
line-height: 12px; line-height: 12px;
font-size: 10px; font-size: 10px;
text-align: center; text-align: center;
border-radius: 6px; border-radius: 6px;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
cursor: pointer; cursor: pointer;
} }
.jmnode-overflow-wrap jmnodes { .jmnode-overflow-wrap jmnodes {
min-width: 420px; min-width: 420px;
} }
.jmnode-overflow-hidden jmnode { .jmnode-overflow-hidden jmnode {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* default theme */ /* default theme */
jmnode { jmnode {
padding: 10px; padding: 10px;
background-color: #fff; background-color: #fff;
color: #333; color: #333;
border-radius: 5px; border-radius: 5px;
box-shadow: 1px 1px 1px #666; box-shadow: 1px 1px 1px #666;
font: 16px/1.125 Verdana, Arial, Helvetica, sans-serif; font: 16px/1.125 Verdana, Arial, Helvetica, sans-serif;
} }
jmnode:hover { jmnode:hover {
box-shadow: 2px 2px 8px #000; box-shadow: 2px 2px 8px #000;
background-color: #ebebeb; background-color: #ebebeb;
color: #333; color: #333;
} }
jmnode.selected { jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
box-shadow: 2px 2px 8px #000; box-shadow: 2px 2px 8px #000;
} }
jmnode.root { jmnode.root {
font-size: 24px; font-size: 24px;
} }
jmexpander { jmexpander {
border-color: gray; border-color: gray;
} }
jmexpander:hover { jmexpander:hover {
border-color: #000; border-color: #000;
} }
@media screen and (max-device-width: 1024px) { @media screen and (max-device-width: 1024px) {
jmnode { jmnode {
padding: 5px; padding: 5px;
border-radius: 3px; border-radius: 3px;
font-size: 14px; font-size: 14px;
} }
jmnode.root { jmnode.root {
font-size: 21px; font-size: 21px;
} }
} }
/* primary theme */ /* primary theme */
jmnodes.theme-primary jmnode { jmnodes.theme-primary jmnode {
background-color: #428bca; background-color: #428bca;
color: #fff; color: #fff;
border-color: #357ebd; border-color: #357ebd;
} }
jmnodes.theme-primary jmnode:hover { jmnodes.theme-primary jmnode:hover {
background-color: #3276b1; background-color: #3276b1;
border-color: #285e8e; border-color: #285e8e;
} }
jmnodes.theme-primary jmnode.selected { jmnodes.theme-primary jmnode.selected {
background-color: #f1c40f; background-color: #f1c40f;
color: #fff; color: #fff;
} }
jmnodes.theme-primary jmnode.root { jmnodes.theme-primary jmnode.root {
} }
jmnodes.theme-primary jmexpander { jmnodes.theme-primary jmexpander {
} }
jmnodes.theme-primary jmexpander:hover { jmnodes.theme-primary jmexpander:hover {
} }
/* warning theme */ /* warning theme */
jmnodes.theme-warning jmnode { jmnodes.theme-warning jmnode {
background-color: #f0ad4e; background-color: #f0ad4e;
border-color: #eea236; border-color: #eea236;
color: #fff; color: #fff;
} }
jmnodes.theme-warning jmnode:hover { jmnodes.theme-warning jmnode:hover {
background-color: #ed9c28; background-color: #ed9c28;
border-color: #d58512; border-color: #d58512;
} }
jmnodes.theme-warning jmnode.selected { jmnodes.theme-warning jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-warning jmnode.root { jmnodes.theme-warning jmnode.root {
} }
jmnodes.theme-warning jmexpander { jmnodes.theme-warning jmexpander {
} }
jmnodes.theme-warning jmexpander:hover { jmnodes.theme-warning jmexpander:hover {
} }
/* danger theme */ /* danger theme */
jmnodes.theme-danger jmnode { jmnodes.theme-danger jmnode {
background-color: #d9534f; background-color: #d9534f;
border-color: #d43f3a; border-color: #d43f3a;
color: #fff; color: #fff;
} }
jmnodes.theme-danger jmnode:hover { jmnodes.theme-danger jmnode:hover {
background-color: #d2322d; background-color: #d2322d;
border-color: #ac2925; border-color: #ac2925;
} }
jmnodes.theme-danger jmnode.selected { jmnodes.theme-danger jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-danger jmnode.root { jmnodes.theme-danger jmnode.root {
} }
jmnodes.theme-danger jmexpander { jmnodes.theme-danger jmexpander {
} }
jmnodes.theme-danger jmexpander:hover { jmnodes.theme-danger jmexpander:hover {
} }
/* success theme */ /* success theme */
jmnodes.theme-success jmnode { jmnodes.theme-success jmnode {
background-color: #5cb85c; background-color: #5cb85c;
border-color: #4cae4c; border-color: #4cae4c;
color: #fff; color: #fff;
} }
jmnodes.theme-success jmnode:hover { jmnodes.theme-success jmnode:hover {
background-color: #47a447; background-color: #47a447;
border-color: #398439; border-color: #398439;
} }
jmnodes.theme-success jmnode.selected { jmnodes.theme-success jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-success jmnode.root { jmnodes.theme-success jmnode.root {
} }
jmnodes.theme-success jmexpander { jmnodes.theme-success jmexpander {
} }
jmnodes.theme-success jmexpander:hover { jmnodes.theme-success jmexpander:hover {
} }
/* info theme */ /* info theme */
jmnodes.theme-info jmnode { jmnodes.theme-info jmnode {
background-color: #5dc0de; background-color: #5dc0de;
border-color: #46b8da; border-color: #46b8da;
color: #fff; color: #fff;
} }
jmnodes.theme-info jmnode:hover { jmnodes.theme-info jmnode:hover {
background-color: #39b3d7; background-color: #39b3d7;
border-color: #269abc; border-color: #269abc;
} }
jmnodes.theme-info jmnode.selected { jmnodes.theme-info jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-info jmnode.root { jmnodes.theme-info jmnode.root {
} }
jmnodes.theme-info jmexpander { jmnodes.theme-info jmexpander {
} }
jmnodes.theme-info jmexpander:hover { jmnodes.theme-info jmexpander:hover {
} }
/* greensea theme */ /* greensea theme */
jmnodes.theme-greensea jmnode { jmnodes.theme-greensea jmnode {
background-color: #1abc9c; background-color: #1abc9c;
color: #fff; color: #fff;
} }
jmnodes.theme-greensea jmnode:hover { jmnodes.theme-greensea jmnode:hover {
background-color: #16a085; background-color: #16a085;
} }
jmnodes.theme-greensea jmnode.selected { jmnodes.theme-greensea jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-greensea jmnode.root { jmnodes.theme-greensea jmnode.root {
} }
jmnodes.theme-greensea jmexpander { jmnodes.theme-greensea jmexpander {
} }
jmnodes.theme-greensea jmexpander:hover { jmnodes.theme-greensea jmexpander:hover {
} }
/* nephrite theme */ /* nephrite theme */
jmnodes.theme-nephrite jmnode { jmnodes.theme-nephrite jmnode {
background-color: #2ecc71; background-color: #2ecc71;
color: #fff; color: #fff;
} }
jmnodes.theme-nephrite jmnode:hover { jmnodes.theme-nephrite jmnode:hover {
background-color: #27ae60; background-color: #27ae60;
} }
jmnodes.theme-nephrite jmnode.selected { jmnodes.theme-nephrite jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-nephrite jmnode.root { jmnodes.theme-nephrite jmnode.root {
} }
jmnodes.theme-nephrite jmexpander { jmnodes.theme-nephrite jmexpander {
} }
jmnodes.theme-nephrite jmexpander:hover { jmnodes.theme-nephrite jmexpander:hover {
} }
/* belizehole theme */ /* belizehole theme */
jmnodes.theme-belizehole jmnode { jmnodes.theme-belizehole jmnode {
background-color: #3498db; background-color: #3498db;
color: #fff; color: #fff;
} }
jmnodes.theme-belizehole jmnode:hover { jmnodes.theme-belizehole jmnode:hover {
background-color: #2980b9; background-color: #2980b9;
} }
jmnodes.theme-belizehole jmnode.selected { jmnodes.theme-belizehole jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-belizehole jmnode.root { jmnodes.theme-belizehole jmnode.root {
} }
jmnodes.theme-belizehole jmexpander { jmnodes.theme-belizehole jmexpander {
} }
jmnodes.theme-belizehole jmexpander:hover { jmnodes.theme-belizehole jmexpander:hover {
} }
/* wisteria theme */ /* wisteria theme */
jmnodes.theme-wisteria jmnode { jmnodes.theme-wisteria jmnode {
background-color: #9b59b6; background-color: #9b59b6;
color: #fff; color: #fff;
} }
jmnodes.theme-wisteria jmnode:hover { jmnodes.theme-wisteria jmnode:hover {
background-color: #8e44ad; background-color: #8e44ad;
} }
jmnodes.theme-wisteria jmnode.selected { jmnodes.theme-wisteria jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-wisteria jmnode.root { jmnodes.theme-wisteria jmnode.root {
} }
jmnodes.theme-wisteria jmexpander { jmnodes.theme-wisteria jmexpander {
} }
jmnodes.theme-wisteria jmexpander:hover { jmnodes.theme-wisteria jmexpander:hover {
} }
/* asphalt theme */ /* asphalt theme */
jmnodes.theme-asphalt jmnode { jmnodes.theme-asphalt jmnode {
background-color: #34495e; background-color: #34495e;
color: #fff; color: #fff;
} }
jmnodes.theme-asphalt jmnode:hover { jmnodes.theme-asphalt jmnode:hover {
background-color: #2c3e50; background-color: #2c3e50;
} }
jmnodes.theme-asphalt jmnode.selected { jmnodes.theme-asphalt jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-asphalt jmnode.root { jmnodes.theme-asphalt jmnode.root {
} }
jmnodes.theme-asphalt jmexpander { jmnodes.theme-asphalt jmexpander {
} }
jmnodes.theme-asphalt jmexpander:hover { jmnodes.theme-asphalt jmexpander:hover {
} }
/* orange theme */ /* orange theme */
jmnodes.theme-orange jmnode { jmnodes.theme-orange jmnode {
background-color: #f1c40f; background-color: #f1c40f;
color: #fff; color: #fff;
} }
jmnodes.theme-orange jmnode:hover { jmnodes.theme-orange jmnode:hover {
background-color: #f39c12; background-color: #f39c12;
} }
jmnodes.theme-orange jmnode.selected { jmnodes.theme-orange jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-orange jmnode.root { jmnodes.theme-orange jmnode.root {
} }
jmnodes.theme-orange jmexpander { jmnodes.theme-orange jmexpander {
} }
jmnodes.theme-orange jmexpander:hover { jmnodes.theme-orange jmexpander:hover {
} }
/* pumpkin theme */ /* pumpkin theme */
jmnodes.theme-pumpkin jmnode { jmnodes.theme-pumpkin jmnode {
background-color: #e67e22; background-color: #e67e22;
color: #fff; color: #fff;
} }
jmnodes.theme-pumpkin jmnode:hover { jmnodes.theme-pumpkin jmnode:hover {
background-color: #d35400; background-color: #d35400;
} }
jmnodes.theme-pumpkin jmnode.selected { jmnodes.theme-pumpkin jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-pumpkin jmnode.root { jmnodes.theme-pumpkin jmnode.root {
} }
jmnodes.theme-pumpkin jmexpander { jmnodes.theme-pumpkin jmexpander {
} }
jmnodes.theme-pumpkin jmexpander:hover { jmnodes.theme-pumpkin jmexpander:hover {
} }
/* pomegranate theme */ /* pomegranate theme */
jmnodes.theme-pomegranate jmnode { jmnodes.theme-pomegranate jmnode {
background-color: #e74c3c; background-color: #e74c3c;
color: #fff; color: #fff;
} }
jmnodes.theme-pomegranate jmnode:hover { jmnodes.theme-pomegranate jmnode:hover {
background-color: #c0392b; background-color: #c0392b;
} }
jmnodes.theme-pomegranate jmnode.selected { jmnodes.theme-pomegranate jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-pomegranate jmnode.root { jmnodes.theme-pomegranate jmnode.root {
} }
jmnodes.theme-pomegranate jmexpander { jmnodes.theme-pomegranate jmexpander {
} }
jmnodes.theme-pomegranate jmexpander:hover { jmnodes.theme-pomegranate jmexpander:hover {
} }
/* clouds theme */ /* clouds theme */
jmnodes.theme-clouds jmnode { jmnodes.theme-clouds jmnode {
background-color: #ecf0f1; background-color: #ecf0f1;
color: #333; color: #333;
} }
jmnodes.theme-clouds jmnode:hover { jmnodes.theme-clouds jmnode:hover {
background-color: #bdc3c7; background-color: #bdc3c7;
} }
jmnodes.theme-clouds jmnode.selected { jmnodes.theme-clouds jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-clouds jmnode.root { jmnodes.theme-clouds jmnode.root {
} }
jmnodes.theme-clouds jmexpander { jmnodes.theme-clouds jmexpander {
} }
jmnodes.theme-clouds jmexpander:hover { jmnodes.theme-clouds jmexpander:hover {
} }
/* asbestos theme */ /* asbestos theme */
jmnodes.theme-asbestos jmnode { jmnodes.theme-asbestos jmnode {
background-color: #95a5a6; background-color: #95a5a6;
color: #fff; color: #fff;
} }
jmnodes.theme-asbestos jmnode:hover { jmnodes.theme-asbestos jmnode:hover {
background-color: #7f8c8d; background-color: #7f8c8d;
} }
jmnodes.theme-asbestos jmnode.selected { jmnodes.theme-asbestos jmnode.selected {
background-color: #11f; background-color: #11f;
color: #fff; color: #fff;
} }
jmnodes.theme-asbestos jmnode.root { jmnodes.theme-asbestos jmnode.root {
} }
jmnodes.theme-asbestos jmexpander { jmnodes.theme-asbestos jmexpander {
} }
jmnodes.theme-asbestos jmexpander:hover { jmnodes.theme-asbestos jmexpander:hover {
} }

View File

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

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

@ -1,49 +1,49 @@
class Dom { class Dom {
constructor(w) { constructor(w) {
this.w = w this.w = w
this.d = w.document this.d = w.document
this.g = function (id) { this.g = function (id) {
return this.d.getElementById(id) return this.d.getElementById(id)
} }
this.c = function (tag) { this.c = function (tag) {
return this.d.createElement(tag) return this.d.createElement(tag)
} }
this.t = function (n, t) { this.t = function (n, t) {
if (n.hasChildNodes()) { if (n.hasChildNodes()) {
n.firstChild.nodeValue = t n.firstChild.nodeValue = t
} else { } else {
n.appendChild(this.d.createTextNode(t)) n.appendChild(this.d.createTextNode(t))
} }
} }
this.h = function (n, t) { this.h = function (n, t) {
if (t instanceof HTMLElement) { if (t instanceof HTMLElement) {
n.innerHTML = '' n.innerHTML = ''
n.appendChild(t) n.appendChild(t)
} else { } else {
n.innerHTML = t n.innerHTML = t
} }
} }
// detect isElement // detect isElement
this.i = function (el) { this.i = function (el) {
return ( return (
!!el && !!el &&
typeof el === 'object' && typeof el === 'object' &&
el.nodeType === 1 && el.nodeType === 1 &&
typeof el.style === 'object' && typeof el.style === 'object' &&
typeof el.ownerDocument === 'object' typeof el.ownerDocument === 'object'
) )
} }
//target,eventType,handler //target,eventType,handler
this.on = function (t, e, h) { this.on = function (t, e, h) {
if (!!t.addEventListener) { if (!!t.addEventListener) {
t.addEventListener(e, h, false) t.addEventListener(e, h, false)
} else { } else {
t.attachEvent('on' + e, h) t.attachEvent('on' + e, h)
} }
} }
} }
} }
export const $ = new Dom(window) 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 { $ } from './jsmind.dom.js'
import { logger } from './jsmind.common.js' import { logger } from './jsmind.common.js'
class SvgGraph { class SvgGraph {
constructor(view) { constructor(view) {
this.view = view this.view = view
this.opts = view.opts this.opts = view.opts
this.e_svg = SvgGraph.c('svg') this.e_svg = SvgGraph.c('svg')
this.e_svg.setAttribute('class', 'jsmind') this.e_svg.setAttribute('class', 'jsmind')
this.size = { w: 0, h: 0 } this.size = { w: 0, h: 0 }
this.lines = [] this.lines = []
this.line_drawing = { this.line_drawing = {
straight: this._line_to, straight: this._line_to,
curved: this._bezier_to, curved: this._bezier_to,
} }
this.init_line_render() this.init_line_render()
} }
static c(tag) { static c(tag) {
return $.d.createElementNS('http://www.w3.org/2000/svg', tag) return $.d.createElementNS('http://www.w3.org/2000/svg', tag)
} }
init_line_render() { init_line_render() {
if (typeof this.opts.custom_line_render === 'function') { if (typeof this.opts.custom_line_render === 'function') {
this.drawing = (path, x1, y1, x2, y2) => { this.drawing = (path, x1, y1, x2, y2) => {
try { try {
this.opts.custom_line_render.call(this, { this.opts.custom_line_render.call(this, {
ctx: path, ctx: path,
start_point: { x: x1, y: y1 }, start_point: { x: x1, y: y1 },
end_point: { x: x2, y: y2 }, end_point: { x: x2, y: y2 },
}) })
} catch (e) { } catch (e) {
logger.error('custom line renderer error: ', e) logger.error('custom line renderer error: ', e)
} }
} }
} else { } else {
this.drawing = this.line_drawing[this.opts.line_style] || this.line_drawing.curved this.drawing = this.line_drawing[this.opts.line_style] || this.line_drawing.curved
} }
} }
element() { element() {
return this.e_svg return this.e_svg
} }
set_size(w, h) { set_size(w, h) {
this.size.w = w this.size.w = w
this.size.h = h this.size.h = h
this.e_svg.setAttribute('width', w) this.e_svg.setAttribute('width', w)
this.e_svg.setAttribute('height', h) this.e_svg.setAttribute('height', h)
} }
clear() { clear() {
var len = this.lines.length var len = this.lines.length
while (len--) { while (len--) {
this.e_svg.removeChild(this.lines[len]) this.e_svg.removeChild(this.lines[len])
} }
this.lines.length = 0 this.lines.length = 0
} }
draw_line(pout, pin, offset, color) { draw_line(pout, pin, offset, color) {
var line = SvgGraph.c('path') var line = SvgGraph.c('path')
line.setAttribute('stroke', color || this.opts.line_color) line.setAttribute('stroke', color || this.opts.line_color)
line.setAttribute('stroke-width', this.opts.line_width) line.setAttribute('stroke-width', this.opts.line_width)
line.setAttribute('fill', 'transparent') line.setAttribute('fill', 'transparent')
this.lines.push(line) this.lines.push(line)
this.e_svg.appendChild(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) 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) { copy_to(dest_canvas_ctx, callback) {
var img = new Image() var img = new Image()
img.onload = function () { img.onload = function () {
dest_canvas_ctx.drawImage(img, 0, 0) dest_canvas_ctx.drawImage(img, 0, 0)
!!callback && callback() !!callback && callback()
} }
img.src = img.src =
'data:image/svg+xml;base64,' + btoa(new XMLSerializer().serializeToString(this.e_svg)) 'data:image/svg+xml;base64,' + btoa(new XMLSerializer().serializeToString(this.e_svg))
} }
_bezier_to(path, x1, y1, x2, y2) { _bezier_to(path, x1, y1, x2, y2) {
path.setAttribute( path.setAttribute(
'd', 'd',
'M ' + 'M ' +
x1 + x1 +
' ' + ' ' +
y1 + y1 +
' C ' + ' C ' +
(x1 + ((x2 - x1) * 2) / 3) + (x1 + ((x2 - x1) * 2) / 3) +
' ' + ' ' +
y1 + y1 +
', ' + ', ' +
x1 + x1 +
' ' + ' ' +
y2 + y2 +
', ' + ', ' +
x2 + x2 +
' ' + ' ' +
y2 y2
) )
} }
_line_to(path, x1, y1, x2, y2) { _line_to(path, x1, y1, x2, y2) {
path.setAttribute('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2) path.setAttribute('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2)
} }
} }
class CanvasGraph { class CanvasGraph {
constructor(view) { constructor(view) {
this.opts = view.opts this.opts = view.opts
this.e_canvas = $.c('canvas') this.e_canvas = $.c('canvas')
this.e_canvas.className = 'jsmind' this.e_canvas.className = 'jsmind'
this.canvas_ctx = this.e_canvas.getContext('2d') this.canvas_ctx = this.e_canvas.getContext('2d')
this.size = { w: 0, h: 0 } this.size = { w: 0, h: 0 }
this.line_drawing = { this.line_drawing = {
straight: this._line_to, straight: this._line_to,
curved: this._bezier_to, curved: this._bezier_to,
} }
this.dpr = view.device_pixel_ratio this.dpr = view.device_pixel_ratio
this.init_line_render() this.init_line_render()
} }
init_line_render() { init_line_render() {
if (typeof this.opts.custom_line_render === 'function') { if (typeof this.opts.custom_line_render === 'function') {
this.drawing = (ctx, x1, y1, x2, y2) => { this.drawing = (ctx, x1, y1, x2, y2) => {
try { try {
this.opts.custom_line_render.call(this, { this.opts.custom_line_render.call(this, {
ctx, ctx,
start_point: { x: x1, y: y1 }, start_point: { x: x1, y: y1 },
end_point: { x: x2, y: y2 }, end_point: { x: x2, y: y2 },
}) })
} catch (e) { } catch (e) {
logger.error('custom line render error: ', e) logger.error('custom line render error: ', e)
} }
} }
} else { } else {
this.drawing = this.line_drawing[this.opts.line_style] || this.line_drawing.curved this.drawing = this.line_drawing[this.opts.line_style] || this.line_drawing.curved
} }
} }
element() { element() {
return this.e_canvas return this.e_canvas
} }
set_size(w, h) { set_size(w, h) {
this.size.w = w this.size.w = w
this.size.h = h this.size.h = h
if (this.e_canvas.width && this.e_canvas.height && this.canvas_ctx.scale) { if (this.e_canvas.width && this.e_canvas.height && this.canvas_ctx.scale) {
this.e_canvas.width = w * this.dpr this.e_canvas.width = w * this.dpr
this.e_canvas.height = h * this.dpr this.e_canvas.height = h * this.dpr
this.e_canvas.style.width = w + 'px' this.e_canvas.style.width = w + 'px'
this.e_canvas.style.height = h + 'px' this.e_canvas.style.height = h + 'px'
this.canvas_ctx.scale(this.dpr, this.dpr) this.canvas_ctx.scale(this.dpr, this.dpr)
} else { } else {
this.e_canvas.width = w this.e_canvas.width = w
this.e_canvas.height = h this.e_canvas.height = h
} }
} }
clear() { clear() {
this.canvas_ctx.clearRect(0, 0, this.size.w, this.size.h) this.canvas_ctx.clearRect(0, 0, this.size.w, this.size.h)
} }
draw_line(pout, pin, offset, color) { draw_line(pout, pin, offset, color) {
var ctx = this.canvas_ctx var ctx = this.canvas_ctx
ctx.strokeStyle = color || this.opts.line_color ctx.strokeStyle = color || this.opts.line_color
ctx.lineWidth = this.opts.line_width ctx.lineWidth = this.opts.line_width
ctx.lineCap = 'round' ctx.lineCap = 'round'
this.drawing(ctx, pin.x + offset.x, pin.y + offset.y, pout.x + offset.x, pout.y + offset.y) 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) { copy_to(dest_canvas_ctx, callback) {
dest_canvas_ctx.drawImage(this.e_canvas, 0, 0, this.size.w, this.size.h) dest_canvas_ctx.drawImage(this.e_canvas, 0, 0, this.size.w, this.size.h)
!!callback && callback() !!callback && callback()
} }
_bezier_to(ctx, x1, y1, x2, y2) { _bezier_to(ctx, x1, y1, x2, y2) {
ctx.beginPath() ctx.beginPath()
ctx.moveTo(x1, y1) ctx.moveTo(x1, y1)
ctx.bezierCurveTo(x1 + ((x2 - x1) * 2) / 3, y1, x1, y2, x2, y2) ctx.bezierCurveTo(x1 + ((x2 - x1) * 2) / 3, y1, x1, y2, x2, y2)
ctx.stroke() ctx.stroke()
} }
_line_to(ctx, x1, y1, x2, y2) { _line_to(ctx, x1, y1, x2, y2) {
ctx.beginPath() ctx.beginPath()
ctx.moveTo(x1, y1) ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2) ctx.lineTo(x2, y2)
ctx.stroke() ctx.stroke()
} }
} }
export function init_graph(view, engine) { export function init_graph(view, engine) {
return engine.toLowerCase() === 'svg' ? new SvgGraph(view) : new CanvasGraph(view) 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' import { logger, Direction, EventType } from './jsmind.common.js'
export class LayoutProvider { export class LayoutProvider {
constructor(jm, options) { constructor(jm, options) {
this.opts = options this.opts = options
this.jm = jm this.jm = jm
this.isside = this.opts.mode == 'side' this.isside = this.opts.mode == 'side'
this.bounds = null this.bounds = null
this.cache_valid = false this.cache_valid = false
} }
init() { init() {
logger.debug('layout.init') logger.debug('layout.init')
} }
reset() { reset() {
logger.debug('layout.reset') logger.debug('layout.reset')
this.bounds = { n: 0, s: 0, w: 0, e: 0 } this.bounds = { n: 0, s: 0, w: 0, e: 0 }
} }
calculate_next_child_direction(node) { calculate_next_child_direction(node) {
if (this.isside) { if (this.isside) {
return Direction.right return Direction.right
} }
var children = node.children || [] var children = node.children || []
var children_len = children.length var children_len = children.length
var r = 0 var r = 0
for (var i = 0; i < children_len; i++) { for (var i = 0; i < children_len; i++) {
if (children[i].direction === Direction.left) { if (children[i].direction === Direction.left) {
r-- r--
} else { } else {
r++ r++
} }
} }
return children_len > 1 && r > 0 ? Direction.left : Direction.right return children_len > 1 && r > 0 ? Direction.left : Direction.right
} }
layout() { layout() {
logger.debug('layout.layout') logger.debug('layout.layout')
this.layout_direction() this.layout_direction()
this.layout_offset() this.layout_offset()
} }
layout_direction() { layout_direction() {
this._layout_direction_root() this._layout_direction_root()
} }
_layout_direction_root() { _layout_direction_root() {
var node = this.jm.mind.root var node = this.jm.mind.root
var layout_data = null var layout_data = null
if ('layout' in node._data) { if ('layout' in node._data) {
layout_data = node._data.layout layout_data = node._data.layout
} else { } else {
layout_data = {} layout_data = {}
node._data.layout = layout_data node._data.layout = layout_data
} }
var children = node.children var children = node.children
var children_count = children.length var children_count = children.length
layout_data.direction = Direction.center layout_data.direction = Direction.center
layout_data.side_index = 0 layout_data.side_index = 0
if (this.isside) { if (this.isside) {
var i = children_count var i = children_count
while (i--) { while (i--) {
this._layout_direction_side(children[i], Direction.right, i) this._layout_direction_side(children[i], Direction.right, i)
} }
} else { } else {
var i = children_count var i = children_count
var subnode = null var subnode = null
while (i--) { while (i--) {
subnode = children[i] subnode = children[i]
if (subnode.direction == Direction.left) { if (subnode.direction == Direction.left) {
this._layout_direction_side(subnode, Direction.left, i) this._layout_direction_side(subnode, Direction.left, i)
} else { } else {
this._layout_direction_side(subnode, Direction.right, i) this._layout_direction_side(subnode, Direction.right, i)
} }
} }
} }
} }
_layout_direction_side(node, direction, side_index) { _layout_direction_side(node, direction, side_index) {
var layout_data = null var layout_data = null
if ('layout' in node._data) { if ('layout' in node._data) {
layout_data = node._data.layout layout_data = node._data.layout
} else { } else {
layout_data = {} layout_data = {}
node._data.layout = layout_data node._data.layout = layout_data
} }
var children = node.children var children = node.children
var children_count = children.length var children_count = children.length
layout_data.direction = direction layout_data.direction = direction
layout_data.side_index = side_index layout_data.side_index = side_index
var i = children_count var i = children_count
while (i--) { while (i--) {
this._layout_direction_side(children[i], direction, i) this._layout_direction_side(children[i], direction, i)
} }
} }
layout_offset() { layout_offset() {
var node = this.jm.mind.root var node = this.jm.mind.root
var layout_data = node._data.layout var layout_data = node._data.layout
layout_data.offset_x = 0 layout_data.offset_x = 0
layout_data.offset_y = 0 layout_data.offset_y = 0
layout_data.outer_height = 0 layout_data.outer_height = 0
var children = node.children var children = node.children
var i = children.length var i = children.length
var left_nodes = [] var left_nodes = []
var right_nodes = [] var right_nodes = []
var subnode = null var subnode = null
while (i--) { while (i--) {
subnode = children[i] subnode = children[i]
if (subnode._data.layout.direction == Direction.right) { if (subnode._data.layout.direction == Direction.right) {
right_nodes.unshift(subnode) right_nodes.unshift(subnode)
} else { } else {
left_nodes.unshift(subnode) left_nodes.unshift(subnode)
} }
} }
layout_data.left_nodes = left_nodes layout_data.left_nodes = left_nodes
layout_data.right_nodes = right_nodes layout_data.right_nodes = right_nodes
layout_data.outer_height_left = this._layout_offset_subnodes(left_nodes) layout_data.outer_height_left = this._layout_offset_subnodes(left_nodes)
layout_data.outer_height_right = this._layout_offset_subnodes(right_nodes) layout_data.outer_height_right = this._layout_offset_subnodes(right_nodes)
this.bounds.e = node._data.view.width / 2 this.bounds.e = node._data.view.width / 2
this.bounds.w = 0 - this.bounds.e this.bounds.w = 0 - this.bounds.e
this.bounds.n = 0 this.bounds.n = 0
this.bounds.s = Math.max(layout_data.outer_height_left, layout_data.outer_height_right) this.bounds.s = Math.max(layout_data.outer_height_left, layout_data.outer_height_right)
} }
// layout both the x and y axis // layout both the x and y axis
_layout_offset_subnodes(nodes) { _layout_offset_subnodes(nodes) {
var total_height = 0 var total_height = 0
var nodes_count = nodes.length var nodes_count = nodes.length
var i = nodes_count var i = nodes_count
var node = null var node = null
var node_outer_height = 0 var node_outer_height = 0
var layout_data = null var layout_data = null
var base_y = 0 var base_y = 0
var pd = null // parent._data var pd = null // parent._data
while (i--) { while (i--) {
node = nodes[i] node = nodes[i]
layout_data = node._data.layout layout_data = node._data.layout
if (pd == null) { if (pd == null) {
pd = node.parent._data pd = node.parent._data
} }
node_outer_height = this._layout_offset_subnodes(node.children) node_outer_height = this._layout_offset_subnodes(node.children)
if (!node.expanded) { if (!node.expanded) {
node_outer_height = 0 node_outer_height = 0
this.set_visible(node.children, false) this.set_visible(node.children, false)
} }
node_outer_height = Math.max(node._data.view.height, node_outer_height) node_outer_height = Math.max(node._data.view.height, node_outer_height)
if (node.children.length > 1) { if (node.children.length > 1) {
node_outer_height += this.opts.cousin_space node_outer_height += this.opts.cousin_space
} }
layout_data.outer_height = node_outer_height layout_data.outer_height = node_outer_height
layout_data.offset_y = base_y - node_outer_height / 2 layout_data.offset_y = base_y - node_outer_height / 2
layout_data.offset_x = layout_data.offset_x =
this.opts.hspace * layout_data.direction + this.opts.hspace * layout_data.direction +
(pd.view.width * (pd.layout.direction + layout_data.direction)) / 2 (pd.view.width * (pd.layout.direction + layout_data.direction)) / 2
if (!node.parent.isroot) { if (!node.parent.isroot) {
layout_data.offset_x += this.opts.pspace * layout_data.direction layout_data.offset_x += this.opts.pspace * layout_data.direction
} }
base_y = base_y - node_outer_height - this.opts.vspace base_y = base_y - node_outer_height - this.opts.vspace
total_height += node_outer_height total_height += node_outer_height
} }
if (nodes_count > 1) { if (nodes_count > 1) {
total_height += this.opts.vspace * (nodes_count - 1) total_height += this.opts.vspace * (nodes_count - 1)
} }
i = nodes_count i = nodes_count
var middle_height = total_height / 2 var middle_height = total_height / 2
while (i--) { while (i--) {
node = nodes[i] node = nodes[i]
node._data.layout.offset_y += middle_height node._data.layout.offset_y += middle_height
} }
return total_height return total_height
} }
// layout the y axis only, for collapse/expand a node // layout the y axis only, for collapse/expand a node
_layout_offset_subnodes_height(nodes) { _layout_offset_subnodes_height(nodes) {
var total_height = 0 var total_height = 0
var nodes_count = nodes.length var nodes_count = nodes.length
var i = nodes_count var i = nodes_count
var node = null var node = null
var node_outer_height = 0 var node_outer_height = 0
var layout_data = null var layout_data = null
var base_y = 0 var base_y = 0
var pd = null // parent._data var pd = null // parent._data
while (i--) { while (i--) {
node = nodes[i] node = nodes[i]
layout_data = node._data.layout layout_data = node._data.layout
if (pd == null) { if (pd == null) {
pd = node.parent._data pd = node.parent._data
} }
node_outer_height = this._layout_offset_subnodes_height(node.children) node_outer_height = this._layout_offset_subnodes_height(node.children)
if (!node.expanded) { if (!node.expanded) {
node_outer_height = 0 node_outer_height = 0
} }
node_outer_height = Math.max(node._data.view.height, node_outer_height) node_outer_height = Math.max(node._data.view.height, node_outer_height)
if (node.children.length > 1) { if (node.children.length > 1) {
node_outer_height += this.opts.cousin_space node_outer_height += this.opts.cousin_space
} }
layout_data.outer_height = node_outer_height layout_data.outer_height = node_outer_height
layout_data.offset_y = base_y - node_outer_height / 2 layout_data.offset_y = base_y - node_outer_height / 2
base_y = base_y - node_outer_height - this.opts.vspace base_y = base_y - node_outer_height - this.opts.vspace
total_height += node_outer_height total_height += node_outer_height
} }
if (nodes_count > 1) { if (nodes_count > 1) {
total_height += this.opts.vspace * (nodes_count - 1) total_height += this.opts.vspace * (nodes_count - 1)
} }
i = nodes_count i = nodes_count
var middle_height = total_height / 2 var middle_height = total_height / 2
while (i--) { while (i--) {
node = nodes[i] node = nodes[i]
node._data.layout.offset_y += middle_height node._data.layout.offset_y += middle_height
} }
return total_height return total_height
} }
get_node_offset(node) { get_node_offset(node) {
var layout_data = node._data.layout var layout_data = node._data.layout
var offset_cache = null var offset_cache = null
if ('_offset_' in layout_data && this.cache_valid) { if ('_offset_' in layout_data && this.cache_valid) {
offset_cache = layout_data._offset_ offset_cache = layout_data._offset_
} else { } else {
offset_cache = { x: -1, y: -1 } offset_cache = { x: -1, y: -1 }
layout_data._offset_ = offset_cache layout_data._offset_ = offset_cache
} }
if (offset_cache.x == -1 || offset_cache.y == -1) { if (offset_cache.x == -1 || offset_cache.y == -1) {
var x = layout_data.offset_x var x = layout_data.offset_x
var y = layout_data.offset_y var y = layout_data.offset_y
if (!node.isroot) { if (!node.isroot) {
var offset_p = this.get_node_offset(node.parent) var offset_p = this.get_node_offset(node.parent)
x += offset_p.x x += offset_p.x
y += offset_p.y y += offset_p.y
} }
offset_cache.x = x offset_cache.x = x
offset_cache.y = y offset_cache.y = y
} }
return offset_cache return offset_cache
} }
get_node_point(node) { get_node_point(node) {
var view_data = node._data.view var view_data = node._data.view
var offset_p = this.get_node_offset(node) var offset_p = this.get_node_offset(node)
var p = {} var p = {}
p.x = offset_p.x + (view_data.width * (node._data.layout.direction - 1)) / 2 p.x = offset_p.x + (view_data.width * (node._data.layout.direction - 1)) / 2
p.y = offset_p.y - view_data.height / 2 p.y = offset_p.y - view_data.height / 2
return p return p
} }
get_node_point_in(node) { get_node_point_in(node) {
var p = this.get_node_offset(node) var p = this.get_node_offset(node)
return p return p
} }
get_node_point_out(node) { get_node_point_out(node) {
var layout_data = node._data.layout var layout_data = node._data.layout
var pout_cache = null var pout_cache = null
if ('_pout_' in layout_data && this.cache_valid) { if ('_pout_' in layout_data && this.cache_valid) {
pout_cache = layout_data._pout_ pout_cache = layout_data._pout_
} else { } else {
pout_cache = { x: -1, y: -1 } pout_cache = { x: -1, y: -1 }
layout_data._pout_ = pout_cache layout_data._pout_ = pout_cache
} }
if (pout_cache.x == -1 || pout_cache.y == -1) { if (pout_cache.x == -1 || pout_cache.y == -1) {
if (node.isroot) { if (node.isroot) {
pout_cache.x = 0 pout_cache.x = 0
pout_cache.y = 0 pout_cache.y = 0
} else { } else {
var view_data = node._data.view var view_data = node._data.view
var offset_p = this.get_node_offset(node) var offset_p = this.get_node_offset(node)
pout_cache.x = pout_cache.x =
offset_p.x + (view_data.width + this.opts.pspace) * node._data.layout.direction offset_p.x + (view_data.width + this.opts.pspace) * node._data.layout.direction
pout_cache.y = offset_p.y pout_cache.y = offset_p.y
} }
} }
return pout_cache return pout_cache
} }
get_expander_point(node) { get_expander_point(node) {
var p = this.get_node_point_out(node) var p = this.get_node_point_out(node)
var ex_p = {} var ex_p = {}
if (node._data.layout.direction == Direction.right) { if (node._data.layout.direction == Direction.right) {
ex_p.x = p.x - this.opts.pspace ex_p.x = p.x - this.opts.pspace
} else { } else {
ex_p.x = p.x ex_p.x = p.x
} }
ex_p.y = p.y - Math.ceil(this.opts.pspace / 2) ex_p.y = p.y - Math.ceil(this.opts.pspace / 2)
return ex_p return ex_p
} }
get_min_size() { get_min_size() {
var nodes = this.jm.mind.nodes var nodes = this.jm.mind.nodes
var node = null var node = null
var pout = null var pout = null
for (var node_id in nodes) { for (var node_id in nodes) {
node = nodes[node_id] node = nodes[node_id]
pout = this.get_node_point_out(node) pout = this.get_node_point_out(node)
if (pout.x > this.bounds.e) { if (pout.x > this.bounds.e) {
this.bounds.e = pout.x this.bounds.e = pout.x
} }
if (pout.x < this.bounds.w) { if (pout.x < this.bounds.w) {
this.bounds.w = pout.x this.bounds.w = pout.x
} }
} }
return { return {
w: this.bounds.e - this.bounds.w, w: this.bounds.e - this.bounds.w,
h: this.bounds.s - this.bounds.n, h: this.bounds.s - this.bounds.n,
} }
} }
toggle_node(node) { toggle_node(node) {
if (node.isroot) { if (node.isroot) {
return return
} }
if (node.expanded) { if (node.expanded) {
this.collapse_node(node) this.collapse_node(node)
} else { } else {
this.expand_node(node) this.expand_node(node)
} }
} }
expand_node(node) { expand_node(node) {
node.expanded = true node.expanded = true
this.part_layout(node) this.part_layout(node)
this.set_visible(node.children, true) this.set_visible(node.children, true)
this.jm.invoke_event_handle(EventType.show, { this.jm.invoke_event_handle(EventType.show, {
evt: 'expand_node', evt: 'expand_node',
data: [], data: [],
node: node.id, node: node.id,
}) })
} }
collapse_node(node) { collapse_node(node) {
node.expanded = false node.expanded = false
this.part_layout(node) this.part_layout(node)
this.set_visible(node.children, false) this.set_visible(node.children, false)
this.jm.invoke_event_handle(EventType.show, { this.jm.invoke_event_handle(EventType.show, {
evt: 'collapse_node', evt: 'collapse_node',
data: [], data: [],
node: node.id, node: node.id,
}) })
} }
expand_all() { expand_all() {
var nodes = this.jm.mind.nodes var nodes = this.jm.mind.nodes
var c = 0 var c = 0
var node var node
for (var node_id in nodes) { for (var node_id in nodes) {
node = nodes[node_id] node = nodes[node_id]
if (!node.expanded) { if (!node.expanded) {
node.expanded = true node.expanded = true
c++ c++
} }
} }
if (c > 0) { if (c > 0) {
var root = this.jm.mind.root var root = this.jm.mind.root
this.part_layout(root) this.part_layout(root)
this.set_visible(root.children, true) this.set_visible(root.children, true)
} }
} }
collapse_all() { collapse_all() {
var nodes = this.jm.mind.nodes var nodes = this.jm.mind.nodes
var c = 0 var c = 0
var node var node
for (var node_id in nodes) { for (var node_id in nodes) {
node = nodes[node_id] node = nodes[node_id]
if (node.expanded && !node.isroot) { if (node.expanded && !node.isroot) {
node.expanded = false node.expanded = false
c++ c++
} }
} }
if (c > 0) { if (c > 0) {
var root = this.jm.mind.root var root = this.jm.mind.root
this.part_layout(root) this.part_layout(root)
this.set_visible(root.children, true) this.set_visible(root.children, true)
} }
} }
expand_to_depth(target_depth, curr_nodes, curr_depth) { expand_to_depth(target_depth, curr_nodes, curr_depth) {
if (target_depth < 1) { if (target_depth < 1) {
return return
} }
var nodes = curr_nodes || this.jm.mind.root.children var nodes = curr_nodes || this.jm.mind.root.children
var depth = curr_depth || 1 var depth = curr_depth || 1
var i = nodes.length var i = nodes.length
var node = null var node = null
while (i--) { while (i--) {
node = nodes[i] node = nodes[i]
if (depth < target_depth) { if (depth < target_depth) {
if (!node.expanded) { if (!node.expanded) {
this.expand_node(node) this.expand_node(node)
} }
this.expand_to_depth(target_depth, node.children, depth + 1) this.expand_to_depth(target_depth, node.children, depth + 1)
} }
if (depth == target_depth) { if (depth == target_depth) {
if (node.expanded) { if (node.expanded) {
this.collapse_node(node) this.collapse_node(node)
} }
} }
} }
} }
part_layout(node) { part_layout(node) {
var root = this.jm.mind.root var root = this.jm.mind.root
if (!!root) { if (!!root) {
var root_layout_data = root._data.layout var root_layout_data = root._data.layout
if (node.isroot) { if (node.isroot) {
root_layout_data.outer_height_right = this._layout_offset_subnodes_height( root_layout_data.outer_height_right = this._layout_offset_subnodes_height(
root_layout_data.right_nodes root_layout_data.right_nodes
) )
root_layout_data.outer_height_left = this._layout_offset_subnodes_height( root_layout_data.outer_height_left = this._layout_offset_subnodes_height(
root_layout_data.left_nodes root_layout_data.left_nodes
) )
} else { } else {
if (node._data.layout.direction == Direction.right) { if (node._data.layout.direction == Direction.right) {
root_layout_data.outer_height_right = this._layout_offset_subnodes_height( root_layout_data.outer_height_right = this._layout_offset_subnodes_height(
root_layout_data.right_nodes root_layout_data.right_nodes
) )
} else { } else {
root_layout_data.outer_height_left = this._layout_offset_subnodes_height( root_layout_data.outer_height_left = this._layout_offset_subnodes_height(
root_layout_data.left_nodes root_layout_data.left_nodes
) )
} }
} }
this.bounds.s = Math.max( this.bounds.s = Math.max(
root_layout_data.outer_height_left, root_layout_data.outer_height_left,
root_layout_data.outer_height_right root_layout_data.outer_height_right
) )
this.cache_valid = false this.cache_valid = false
} else { } else {
logger.warn('can not found root node') logger.warn('can not found root node')
} }
} }
set_visible(nodes, visible) { set_visible(nodes, visible) {
var i = nodes.length var i = nodes.length
var node = null var node = null
var layout_data = null var layout_data = null
while (i--) { while (i--) {
node = nodes[i] node = nodes[i]
layout_data = node._data.layout layout_data = node._data.layout
if (node.expanded) { if (node.expanded) {
this.set_visible(node.children, visible) this.set_visible(node.children, visible)
} else { } else {
this.set_visible(node.children, false) this.set_visible(node.children, false)
} }
if (!node.isroot) { if (!node.isroot) {
node._data.layout.visible = visible node._data.layout.visible = visible
} }
} }
} }
is_expand(node) { is_expand(node) {
return node.expanded return node.expanded
} }
is_visible(node) { is_visible(node) {
var layout_data = node._data.layout var layout_data = node._data.layout
if ('visible' in layout_data && !layout_data.visible) { if ('visible' in layout_data && !layout_data.visible) {
return false return false
} else { } else {
return true 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 { Node } from './jsmind.node.js'
import { logger, Direction } from './jsmind.common.js' import { logger, Direction } from './jsmind.common.js'
export class Mind { export class Mind {
constructor() { constructor() {
this.name = null this.name = null
this.author = null this.author = null
this.version = null this.version = null
this.root = null this.root = null
this.selected = null this.selected = null
this.nodes = {} this.nodes = {}
} }
get_node(node_id) { get_node(node_id) {
if (node_id in this.nodes) { if (node_id in this.nodes) {
return this.nodes[node_id] return this.nodes[node_id]
} else { } else {
logger.warn('the node[id=' + node_id + '] can not be found') logger.warn('the node[id=' + node_id + '] can not be found')
return null return null
} }
} }
set_root(node_id, topic, data) { set_root(node_id, topic, data) {
if (this.root == null) { if (this.root == null) {
this.root = new Node(node_id, 0, topic, data, true) this.root = new Node(node_id, 0, topic, data, true)
this._put_node(this.root) this._put_node(this.root)
return this.root return this.root
} else { } else {
logger.error('root node is already exist') logger.error('root node is already exist')
return null return null
} }
} }
add_node(parent_node, node_id, topic, data, direction, expanded, idx) { add_node(parent_node, node_id, topic, data, direction, expanded, idx) {
if (!Node.is_node(parent_node)) { if (!Node.is_node(parent_node)) {
logger.error('the parent_node ' + parent_node + ' is not a node.') logger.error('the parent_node ' + parent_node + ' is not a node.')
return null return null
} }
var node_index = idx || -1 var node_index = idx || -1
var node = new Node( var node = new Node(
node_id, node_id,
node_index, node_index,
topic, topic,
data, data,
false, false,
parent_node, parent_node,
parent_node.direction, parent_node.direction,
expanded expanded
) )
if (parent_node.isroot) { if (parent_node.isroot) {
node.direction = direction || Direction.right node.direction = direction || Direction.right
} }
if (this._put_node(node)) { if (this._put_node(node)) {
parent_node.children.push(node) parent_node.children.push(node)
this._update_index(parent_node) this._update_index(parent_node)
} else { } else {
logger.error("fail, the node id '" + node.id + "' has been already exist.") logger.error("fail, the node id '" + node.id + "' has been already exist.")
node = null node = null
} }
return node return node
} }
insert_node_before(node_before, node_id, topic, data, direction) { insert_node_before(node_before, node_id, topic, data, direction) {
if (!Node.is_node(node_before)) { if (!Node.is_node(node_before)) {
logger.error('the node_before ' + node_before + ' is not a node.') logger.error('the node_before ' + node_before + ' is not a node.')
return null return null
} }
var node_index = node_before.index - 0.5 var node_index = node_before.index - 0.5
return this.add_node(node_before.parent, node_id, topic, data, direction, true, node_index) return this.add_node(node_before.parent, node_id, topic, data, direction, true, node_index)
} }
get_node_before(node) { get_node_before(node) {
if (!Node.is_node(node)) { if (!Node.is_node(node)) {
var the_node = this.get_node(node) var the_node = this.get_node(node)
if (!the_node) { if (!the_node) {
logger.error('the node[id=' + node + '] can not be found.') logger.error('the node[id=' + node + '] can not be found.')
return null return null
} else { } else {
return this.get_node_before(the_node) return this.get_node_before(the_node)
} }
} }
if (node.isroot) { if (node.isroot) {
return null return null
} }
var idx = node.index - 2 var idx = node.index - 2
if (idx >= 0) { if (idx >= 0) {
return node.parent.children[idx] return node.parent.children[idx]
} else { } else {
return null return null
} }
} }
insert_node_after(node_after, node_id, topic, data, direction) { insert_node_after(node_after, node_id, topic, data, direction) {
if (!Node.is_node(node_after)) { if (!Node.is_node(node_after)) {
logger.error('the node_after ' + node_after + ' is not a node.') logger.error('the node_after ' + node_after + ' is not a node.')
return null return null
} }
var node_index = node_after.index + 0.5 var node_index = node_after.index + 0.5
return this.add_node(node_after.parent, node_id, topic, data, direction, true, node_index) return this.add_node(node_after.parent, node_id, topic, data, direction, true, node_index)
} }
get_node_after(node) { get_node_after(node) {
if (!Node.is_node(node)) { if (!Node.is_node(node)) {
var the_node = this.get_node(node) var the_node = this.get_node(node)
if (!the_node) { if (!the_node) {
logger.error('the node[id=' + node + '] can not be found.') logger.error('the node[id=' + node + '] can not be found.')
return null return null
} else { } else {
return this.get_node_after(the_node) return this.get_node_after(the_node)
} }
} }
if (node.isroot) { if (node.isroot) {
return null return null
} }
var idx = node.index var idx = node.index
var brothers = node.parent.children var brothers = node.parent.children
if (brothers.length > idx) { if (brothers.length > idx) {
return node.parent.children[idx] return node.parent.children[idx]
} else { } else {
return null return null
} }
} }
move_node(node, before_id, parent_id, direction) { move_node(node, before_id, parent_id, direction) {
if (!Node.is_node(node)) { if (!Node.is_node(node)) {
logger.error('the parameter node ' + node + ' is not a node.') logger.error('the parameter node ' + node + ' is not a node.')
return null return null
} }
if (!parent_id) { if (!parent_id) {
parent_id = node.parent.id parent_id = node.parent.id
} }
return this._move_node(node, before_id, parent_id, direction) return this._move_node(node, before_id, parent_id, direction)
} }
_flow_node_direction(node, direction) { _flow_node_direction(node, direction) {
if (typeof direction === 'undefined') { if (typeof direction === 'undefined') {
direction = node.direction direction = node.direction
} else { } else {
node.direction = direction node.direction = direction
} }
var len = node.children.length var len = node.children.length
while (len--) { while (len--) {
this._flow_node_direction(node.children[len], direction) this._flow_node_direction(node.children[len], direction)
} }
} }
_move_node_internal(node, before_id) { _move_node_internal(node, before_id) {
if (!!node && !!before_id) { if (!!node && !!before_id) {
if (before_id == '_last_') { if (before_id == '_last_') {
node.index = -1 node.index = -1
this._update_index(node.parent) this._update_index(node.parent)
} else if (before_id == '_first_') { } else if (before_id == '_first_') {
node.index = 0 node.index = 0
this._update_index(node.parent) this._update_index(node.parent)
} else { } else {
var node_before = !!before_id ? this.get_node(before_id) : null var node_before = !!before_id ? this.get_node(before_id) : null
if ( if (
node_before != null && node_before != null &&
node_before.parent != null && node_before.parent != null &&
node_before.parent.id == node.parent.id node_before.parent.id == node.parent.id
) { ) {
node.index = node_before.index - 0.5 node.index = node_before.index - 0.5
this._update_index(node.parent) this._update_index(node.parent)
} }
} }
} }
return node return node
} }
_move_node(node, before_id, parent_id, direction) { _move_node(node, before_id, parent_id, direction) {
if (!!node && !!parent_id) { if (!!node && !!parent_id) {
var parent_node = this.get_node(parent_id) var parent_node = this.get_node(parent_id)
if (Node.inherited(node, parent_node)) { if (Node.inherited(node, parent_node)) {
logger.error('can not move a node to its children') logger.error('can not move a node to its children')
return null return null
} }
if (node.parent.id != parent_id) { if (node.parent.id != parent_id) {
// remove from parent's children // remove from parent's children
var sibling = node.parent.children var sibling = node.parent.children
var si = sibling.length var si = sibling.length
while (si--) { while (si--) {
if (sibling[si].id == node.id) { if (sibling[si].id == node.id) {
sibling.splice(si, 1) sibling.splice(si, 1)
break break
} }
} }
let origin_parent = node.parent let origin_parent = node.parent
node.parent = parent_node node.parent = parent_node
parent_node.children.push(node) parent_node.children.push(node)
this._update_index(origin_parent) this._update_index(origin_parent)
} }
if (node.parent.isroot) { if (node.parent.isroot) {
if (direction == Direction.left) { if (direction == Direction.left) {
node.direction = direction node.direction = direction
} else { } else {
node.direction = Direction.right node.direction = Direction.right
} }
} else { } else {
node.direction = node.parent.direction node.direction = node.parent.direction
} }
this._move_node_internal(node, before_id) this._move_node_internal(node, before_id)
this._flow_node_direction(node) this._flow_node_direction(node)
} }
return node return node
} }
remove_node(node) { remove_node(node) {
if (!Node.is_node(node)) { if (!Node.is_node(node)) {
logger.error('the parameter node ' + node + ' is not a node.') logger.error('the parameter node ' + node + ' is not a node.')
return false return false
} }
if (node.isroot) { if (node.isroot) {
logger.error('fail, can not remove root node') logger.error('fail, can not remove root node')
return false return false
} }
if (this.selected != null && this.selected.id == node.id) { if (this.selected != null && this.selected.id == node.id) {
this.selected = null this.selected = null
} }
// clean all subordinate nodes // clean all subordinate nodes
var children = node.children var children = node.children
var ci = children.length var ci = children.length
while (ci--) { while (ci--) {
this.remove_node(children[ci]) this.remove_node(children[ci])
} }
// clean all children // clean all children
children.length = 0 children.length = 0
var node_parent = node.parent var node_parent = node.parent
// remove from parent's children // remove from parent's children
var sibling = node_parent.children var sibling = node_parent.children
var si = sibling.length var si = sibling.length
while (si--) { while (si--) {
if (sibling[si].id == node.id) { if (sibling[si].id == node.id) {
sibling.splice(si, 1) sibling.splice(si, 1)
break break
} }
} }
// remove from global nodes // remove from global nodes
delete this.nodes[node.id] delete this.nodes[node.id]
// clean all properties // clean all properties
for (var k in node) { for (var k in node) {
delete node[k] delete node[k]
} }
// remove it's self // remove it's self
node = null node = null
this._update_index(node_parent) this._update_index(node_parent)
return true return true
} }
_put_node(node) { _put_node(node) {
if (node.id in this.nodes) { if (node.id in this.nodes) {
logger.warn("the node_id '" + node.id + "' has been already exist.") logger.warn("the node_id '" + node.id + "' has been already exist.")
return false return false
} else { } else {
this.nodes[node.id] = node this.nodes[node.id] = node
return true return true
} }
} }
_update_index(node) { _update_index(node) {
if (node instanceof Node) { if (node instanceof Node) {
node.children.sort(Node.compare) node.children.sort(Node.compare)
for (var i = 0; i < node.children.length; i++) { for (var i = 0; i < node.children.length; i++) {
node.children[i].index = i + 1 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' import { logger } from './jsmind.common.js'
export class Node { export class Node {
constructor(sId, iIndex, sTopic, oData, bIsRoot, oParent, eDirection, bExpanded) { constructor(sId, iIndex, sTopic, oData, bIsRoot, oParent, eDirection, bExpanded) {
if (!sId) { if (!sId) {
logger.error('invalid node id') logger.error('invalid node id')
return return
} }
if (typeof iIndex != 'number') { if (typeof iIndex != 'number') {
logger.error('invalid node index') logger.error('invalid node index')
return return
} }
if (typeof bExpanded === 'undefined') { if (typeof bExpanded === 'undefined') {
bExpanded = true bExpanded = true
} }
this.id = sId this.id = sId
this.index = iIndex this.index = iIndex
this.topic = sTopic this.topic = sTopic
this.data = oData || {} this.data = oData || {}
this.isroot = bIsRoot this.isroot = bIsRoot
this.parent = oParent this.parent = oParent
this.direction = eDirection this.direction = eDirection
this.expanded = !!bExpanded this.expanded = !!bExpanded
this.children = [] this.children = []
this._data = {} this._data = {}
} }
get_location() { get_location() {
var vd = this._data.view var vd = this._data.view
return { return {
x: vd.abs_x, x: vd.abs_x,
y: vd.abs_y, y: vd.abs_y,
} }
} }
get_size() { get_size() {
var vd = this._data.view var vd = this._data.view
return { return {
w: vd.width, w: vd.width,
h: vd.height, h: vd.height,
} }
} }
static compare(node1, node2) { static compare(node1, node2) {
// '-1' is always the latest // '-1' is always the latest
var r = 0 var r = 0
var i1 = node1.index var i1 = node1.index
var i2 = node2.index var i2 = node2.index
if (i1 >= 0 && i2 >= 0) { if (i1 >= 0 && i2 >= 0) {
r = i1 - i2 r = i1 - i2
} else if (i1 == -1 && i2 == -1) { } else if (i1 == -1 && i2 == -1) {
r = 0 r = 0
} else if (i1 == -1) { } else if (i1 == -1) {
r = 1 r = 1
} else if (i2 == -1) { } else if (i2 == -1) {
r = -1 r = -1
} else { } else {
r = 0 r = 0
} }
return r return r
} }
static inherited(parent_node, node) { static inherited(parent_node, node) {
if (!!parent_node && !!node) { if (!!parent_node && !!node) {
if (parent_node.id === node.id) { if (parent_node.id === node.id) {
return true return true
} }
if (parent_node.isroot) { if (parent_node.isroot) {
return true return true
} }
var pid = parent_node.id var pid = parent_node.id
var p = node var p = node
while (!p.isroot) { while (!p.isroot) {
p = p.parent p = p.parent
if (p.id === pid) { if (p.id === pid) {
return true return true
} }
} }
} }
return false return false
} }
static is_node(n) { static is_node(n) {
return !!n && n instanceof Node 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' import { util } from './jsmind.util.js'
const default_options = { const default_options = {
container: '', // id of the container container: '', // id of the container
editable: false, // you can change it in your options editable: false, // you can change it in your options
theme: null, theme: null,
mode: 'full', // full or side mode: 'full', // full or side
support_html: true, support_html: true,
log_level: 'info', log_level: 'info',
view: { view: {
engine: 'canvas', engine: 'canvas',
enable_device_pixel_ratio: false, enable_device_pixel_ratio: false,
hmargin: 100, hmargin: 100,
vmargin: 50, vmargin: 50,
line_width: 2, line_width: 2,
line_color: '#555', line_color: '#555',
line_style: 'curved', // [straight | curved] line_style: 'curved', // [straight | curved]
draggable: false, // drag the mind map with your mouse, when it's larger that the container 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. 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] node_overflow: 'hidden', // [hidden | wrap]
zoom: { zoom: {
min: 0.5, min: 0.5,
max: 2.1, max: 2.1,
step: 0.1, step: 0.1,
}, },
custom_node_render: null, custom_node_render: null,
expander_style: 'char', // [char | number] expander_style: 'char', // [char | number]
}, },
layout: { layout: {
hspace: 30, hspace: 30,
vspace: 20, vspace: 20,
pspace: 13, pspace: 13,
cousin_space: 0, cousin_space: 0,
}, },
default_event_handle: { default_event_handle: {
enable_mousedown_handle: true, enable_mousedown_handle: true,
enable_click_handle: true, enable_click_handle: true,
enable_dblclick_handle: true, enable_dblclick_handle: true,
enable_mousewheel_handle: true, enable_mousewheel_handle: true,
}, },
shortcut: { shortcut: {
enable: true, enable: true,
handles: {}, handles: {},
mapping: { mapping: {
addchild: [45, 4096 + 13], // Insert, Ctrl+Enter addchild: [45, 4096 + 13], // Insert, Ctrl+Enter
addbrother: 13, // Enter addbrother: 13, // Enter
editnode: 113, // F2 editnode: 113, // F2
delnode: 46, // Delete delnode: 46, // Delete
toggle: 32, // Space toggle: 32, // Space
left: 37, // Left left: 37, // Left
up: 38, // Up up: 38, // Up
right: 39, // Right right: 39, // Right
down: 40, // Down down: 40, // Down
}, },
}, },
plugin: {}, plugin: {},
} }
export function merge_option(options) { export function merge_option(options) {
var opts = {} var opts = {}
util.json.merge(opts, default_options) util.json.merge(opts, default_options)
util.json.merge(opts, options) util.json.merge(opts, options)
if (!opts.container) { if (!opts.container) {
throw new Error('the options.container should not be null or empty.') throw new Error('the options.container should not be null or empty.')
} }
return opts 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' import { $ } from './jsmind.dom.js'
const plugin_data = { const plugin_data = {
plugins: [], plugins: [],
} }
export function register(plugin) { export function register(plugin) {
if (!(plugin instanceof Plugin)) { if (!(plugin instanceof Plugin)) {
throw new Error('can not register plugin, it is not an instance of 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)) { if (plugin_data.plugins.map((p) => p.name).includes(plugin.name)) {
throw new Error('can not register plugin ' + plugin.name + ': plugin name already exist') throw new Error('can not register plugin ' + plugin.name + ': plugin name already exist')
} }
plugin_data.plugins.push(plugin) plugin_data.plugins.push(plugin)
} }
export function apply(jm, options) { export function apply(jm, options) {
$.w.setTimeout(function () { $.w.setTimeout(function () {
_apply(jm, options) _apply(jm, options)
}, 0) }, 0)
} }
function _apply(jm, options) { function _apply(jm, options) {
plugin_data.plugins.forEach((p) => p.fn_init(jm, options[p.name])) plugin_data.plugins.forEach((p) => p.fn_init(jm, options[p.name]))
} }
export class Plugin { export class Plugin {
// function fn_init(jm, options){ } // function fn_init(jm, options){ }
constructor(name, fn_init) { constructor(name, fn_init) {
if (!name) { if (!name) {
throw new Error('plugin must has a name') throw new Error('plugin must has a name')
} }
if (!fn_init || typeof fn_init !== 'function') { if (!fn_init || typeof fn_init !== 'function') {
throw new Error('plugin must has an init function') throw new Error('plugin must has an init function')
} }
this.name = name this.name = name
this.fn_init = fn_init this.fn_init = fn_init
} }
} }

View File

@ -1,188 +1,188 @@
import { $ } from './jsmind.dom.js' import { $ } from './jsmind.dom.js'
import { util } from './jsmind.util.js' import { util } from './jsmind.util.js'
import { Direction } from './jsmind.common.js' import { Direction } from './jsmind.common.js'
export class ShortcutProvider { export class ShortcutProvider {
constructor(jm, options) { constructor(jm, options) {
this.jm = jm this.jm = jm
this.opts = options this.opts = options
this.mapping = options.mapping this.mapping = options.mapping
this.handles = options.handles this.handles = options.handles
this._newid = null this._newid = null
this._mapping = {} this._mapping = {}
} }
init() { init() {
$.on(this.jm.view.e_panel, 'keydown', this.handler.bind(this)) $.on(this.jm.view.e_panel, 'keydown', this.handler.bind(this))
this.handles['addchild'] = this.handle_addchild this.handles['addchild'] = this.handle_addchild
this.handles['addbrother'] = this.handle_addbrother this.handles['addbrother'] = this.handle_addbrother
this.handles['editnode'] = this.handle_editnode this.handles['editnode'] = this.handle_editnode
this.handles['delnode'] = this.handle_delnode this.handles['delnode'] = this.handle_delnode
this.handles['toggle'] = this.handle_toggle this.handles['toggle'] = this.handle_toggle
this.handles['up'] = this.handle_up this.handles['up'] = this.handle_up
this.handles['down'] = this.handle_down this.handles['down'] = this.handle_down
this.handles['left'] = this.handle_left this.handles['left'] = this.handle_left
this.handles['right'] = this.handle_right this.handles['right'] = this.handle_right
for (var handle in this.mapping) { for (var handle in this.mapping) {
if (!!this.mapping[handle] && handle in this.handles) { if (!!this.mapping[handle] && handle in this.handles) {
let keys = this.mapping[handle] let keys = this.mapping[handle]
if (!Array.isArray(keys)) { if (!Array.isArray(keys)) {
keys = [keys] keys = [keys]
} }
for (let key of keys) { for (let key of keys) {
this._mapping[key] = this.handles[handle] this._mapping[key] = this.handles[handle]
} }
} }
} }
if (typeof this.opts.id_generator === 'function') { if (typeof this.opts.id_generator === 'function') {
this._newid = this.opts.id_generator this._newid = this.opts.id_generator
} else { } else {
this._newid = util.uuid.newid this._newid = util.uuid.newid
} }
} }
enable_shortcut() { enable_shortcut() {
this.opts.enable = true this.opts.enable = true
} }
disable_shortcut() { disable_shortcut() {
this.opts.enable = false this.opts.enable = false
} }
handler(e) { handler(e) {
if (e.which == 9) { if (e.which == 9) {
e.preventDefault() e.preventDefault()
} //prevent tab to change focus in browser } //prevent tab to change focus in browser
if (this.jm.view.is_editing()) { if (this.jm.view.is_editing()) {
return return
} }
var evt = e || event var evt = e || event
if (!this.opts.enable) { if (!this.opts.enable) {
return true return true
} }
var kc = var kc =
evt.keyCode + evt.keyCode +
(evt.metaKey << 13) + (evt.metaKey << 13) +
(evt.ctrlKey << 12) + (evt.ctrlKey << 12) +
(evt.altKey << 11) + (evt.altKey << 11) +
(evt.shiftKey << 10) (evt.shiftKey << 10)
if (kc in this._mapping) { if (kc in this._mapping) {
this._mapping[kc].call(this, this.jm, e) this._mapping[kc].call(this, this.jm, e)
} }
} }
handle_addchild(_jm, e) { handle_addchild(_jm, e) {
var selected_node = _jm.get_selected_node() var selected_node = _jm.get_selected_node()
if (!!selected_node) { if (!!selected_node) {
var node_id = this._newid() var node_id = this._newid()
var node = _jm.add_node(selected_node, node_id, 'New Node') var node = _jm.add_node(selected_node, node_id, 'New Node')
if (!!node) { if (!!node) {
_jm.select_node(node_id) _jm.select_node(node_id)
_jm.begin_edit(node_id) _jm.begin_edit(node_id)
} }
} }
} }
handle_addbrother(_jm, e) { handle_addbrother(_jm, e) {
var selected_node = _jm.get_selected_node() var selected_node = _jm.get_selected_node()
if (!!selected_node && !selected_node.isroot) { if (!!selected_node && !selected_node.isroot) {
var node_id = this._newid() var node_id = this._newid()
var node = _jm.insert_node_after(selected_node, node_id, 'New Node') var node = _jm.insert_node_after(selected_node, node_id, 'New Node')
if (!!node) { if (!!node) {
_jm.select_node(node_id) _jm.select_node(node_id)
_jm.begin_edit(node_id) _jm.begin_edit(node_id)
} }
} }
} }
handle_editnode(_jm, e) { handle_editnode(_jm, e) {
var selected_node = _jm.get_selected_node() var selected_node = _jm.get_selected_node()
if (!!selected_node) { if (!!selected_node) {
_jm.begin_edit(selected_node) _jm.begin_edit(selected_node)
} }
} }
handle_delnode(_jm, e) { handle_delnode(_jm, e) {
var selected_node = _jm.get_selected_node() var selected_node = _jm.get_selected_node()
if (!!selected_node && !selected_node.isroot) { if (!!selected_node && !selected_node.isroot) {
_jm.select_node(selected_node.parent) _jm.select_node(selected_node.parent)
_jm.remove_node(selected_node) _jm.remove_node(selected_node)
} }
} }
handle_toggle(_jm, e) { handle_toggle(_jm, e) {
var evt = e || event var evt = e || event
var selected_node = _jm.get_selected_node() var selected_node = _jm.get_selected_node()
if (!!selected_node) { if (!!selected_node) {
_jm.toggle_node(selected_node.id) _jm.toggle_node(selected_node.id)
evt.stopPropagation() evt.stopPropagation()
evt.preventDefault() evt.preventDefault()
} }
} }
handle_up(_jm, e) { handle_up(_jm, e) {
var evt = e || event var evt = e || event
var selected_node = _jm.get_selected_node() var selected_node = _jm.get_selected_node()
if (!!selected_node) { if (!!selected_node) {
var up_node = _jm.find_node_before(selected_node) var up_node = _jm.find_node_before(selected_node)
if (!up_node) { if (!up_node) {
var np = _jm.find_node_before(selected_node.parent) var np = _jm.find_node_before(selected_node.parent)
if (!!np && np.children.length > 0) { if (!!np && np.children.length > 0) {
up_node = np.children[np.children.length - 1] up_node = np.children[np.children.length - 1]
} }
} }
if (!!up_node) { if (!!up_node) {
_jm.select_node(up_node) _jm.select_node(up_node)
} }
evt.stopPropagation() evt.stopPropagation()
evt.preventDefault() evt.preventDefault()
} }
} }
handle_down(_jm, e) { handle_down(_jm, e) {
var evt = e || event var evt = e || event
var selected_node = _jm.get_selected_node() var selected_node = _jm.get_selected_node()
if (!!selected_node) { if (!!selected_node) {
var down_node = _jm.find_node_after(selected_node) var down_node = _jm.find_node_after(selected_node)
if (!down_node) { if (!down_node) {
var np = _jm.find_node_after(selected_node.parent) var np = _jm.find_node_after(selected_node.parent)
if (!!np && np.children.length > 0) { if (!!np && np.children.length > 0) {
down_node = np.children[0] down_node = np.children[0]
} }
} }
if (!!down_node) { if (!!down_node) {
_jm.select_node(down_node) _jm.select_node(down_node)
} }
evt.stopPropagation() evt.stopPropagation()
evt.preventDefault() evt.preventDefault()
} }
} }
handle_left(_jm, e) { handle_left(_jm, e) {
this._handle_direction(_jm, e, Direction.left) this._handle_direction(_jm, e, Direction.left)
} }
handle_right(_jm, e) { handle_right(_jm, e) {
this._handle_direction(_jm, e, Direction.right) this._handle_direction(_jm, e, Direction.right)
} }
_handle_direction(_jm, e, d) { _handle_direction(_jm, e, d) {
var evt = e || event var evt = e || event
var selected_node = _jm.get_selected_node() var selected_node = _jm.get_selected_node()
var node = null var node = null
if (!!selected_node) { if (!!selected_node) {
if (selected_node.isroot) { if (selected_node.isroot) {
var c = selected_node.children var c = selected_node.children
var children = [] var children = []
for (var i = 0; i < c.length; i++) { for (var i = 0; i < c.length; i++) {
if (c[i].direction === d) { if (c[i].direction === d) {
children.push(i) children.push(i)
} }
} }
node = c[children[Math.floor((children.length - 1) / 2)]] node = c[children[Math.floor((children.length - 1) / 2)]]
} else if (selected_node.direction === d) { } else if (selected_node.direction === d) {
var children = selected_node.children var children = selected_node.children
var children_count = children.length var children_count = children.length
if (children_count > 0) { if (children_count > 0) {
node = children[Math.floor((children_count - 1) / 2)] node = children[Math.floor((children_count - 1) / 2)]
} }
} else { } else {
node = selected_node.parent node = selected_node.parent
} }
if (!!node) { if (!!node) {
_jm.select_node(node) _jm.select_node(node)
} }
evt.stopPropagation() evt.stopPropagation()
evt.preventDefault() 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' import { $ } from './jsmind.dom.js'
export const util = { export const util = {
file: { file: {
read: function (file_data, fn_callback) { read: function (file_data, fn_callback) {
var reader = new FileReader() var reader = new FileReader()
reader.onload = function () { reader.onload = function () {
if (typeof fn_callback === 'function') { if (typeof fn_callback === 'function') {
fn_callback(this.result, file_data.name) fn_callback(this.result, file_data.name)
} }
} }
reader.readAsText(file_data) reader.readAsText(file_data)
}, },
save: function (file_data, type, name) { save: function (file_data, type, name) {
var blob var blob
if (typeof $.w.Blob === 'function') { if (typeof $.w.Blob === 'function') {
blob = new Blob([file_data], { type: type }) blob = new Blob([file_data], { type: type })
} else { } else {
var BlobBuilder = var BlobBuilder =
$.w.BlobBuilder || $.w.BlobBuilder ||
$.w.MozBlobBuilder || $.w.MozBlobBuilder ||
$.w.WebKitBlobBuilder || $.w.WebKitBlobBuilder ||
$.w.MSBlobBuilder $.w.MSBlobBuilder
var bb = new BlobBuilder() var bb = new BlobBuilder()
bb.append(file_data) bb.append(file_data)
blob = bb.getBlob(type) blob = bb.getBlob(type)
} }
if (navigator.msSaveBlob) { if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, name) navigator.msSaveBlob(blob, name)
} else { } else {
var URL = $.w.URL || $.w.webkitURL var URL = $.w.URL || $.w.webkitURL
var blob_url = URL.createObjectURL(blob) var blob_url = URL.createObjectURL(blob)
var anchor = $.c('a') var anchor = $.c('a')
if ('download' in anchor) { if ('download' in anchor) {
anchor.style.visibility = 'hidden' anchor.style.visibility = 'hidden'
anchor.href = blob_url anchor.href = blob_url
anchor.download = name anchor.download = name
$.d.body.appendChild(anchor) $.d.body.appendChild(anchor)
var evt = $.d.createEvent('MouseEvents') var evt = $.d.createEvent('MouseEvents')
evt.initEvent('click', true, true) evt.initEvent('click', true, true)
anchor.dispatchEvent(evt) anchor.dispatchEvent(evt)
$.d.body.removeChild(anchor) $.d.body.removeChild(anchor)
} else { } else {
location.href = blob_url location.href = blob_url
} }
} }
}, },
}, },
json: { json: {
json2string: function (json) { json2string: function (json) {
return JSON.stringify(json) return JSON.stringify(json)
}, },
string2json: function (json_str) { string2json: function (json_str) {
return JSON.parse(json_str) return JSON.parse(json_str)
}, },
merge: function (b, a) { merge: function (b, a) {
for (var o in a) { for (var o in a) {
if (o in b) { if (o in b) {
if ( if (
typeof b[o] === 'object' && typeof b[o] === 'object' &&
Object.prototype.toString.call(b[o]).toLowerCase() == '[object object]' && Object.prototype.toString.call(b[o]).toLowerCase() == '[object object]' &&
!b[o].length !b[o].length
) { ) {
util.json.merge(b[o], a[o]) util.json.merge(b[o], a[o])
} else { } else {
b[o] = a[o] b[o] = a[o]
} }
} else { } else {
b[o] = a[o] b[o] = a[o]
} }
} }
return b return b
}, },
}, },
uuid: { uuid: {
newid: function () { newid: function () {
return ( return (
new Date().getTime().toString(16) + Math.random().toString(16).substring(2) new Date().getTime().toString(16) + Math.random().toString(16).substring(2)
).substring(2, 18) ).substring(2, 18)
}, },
}, },
text: { text: {
is_empty: function (s) { is_empty: function (s) {
if (!s) { if (!s) {
return true return true
} }
return s.replace(/\s*/, '').length == 0 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' import jsMind from '../jsmind.js'
if (!jsMind) { if (!jsMind) {
throw new Error('jsMind is not defined') throw new Error('jsMind is not defined')
} }
const $ = jsMind.$ const $ = jsMind.$
const clear_selection = const clear_selection =
'getSelection' in $.w 'getSelection' in $.w
? function () { ? function () {
$.w.getSelection().removeAllRanges() $.w.getSelection().removeAllRanges()
} }
: function () { : function () {
$.d.selection.empty() $.d.selection.empty()
} }
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {
line_width: 5, line_width: 5,
line_color: 'rgba(0,0,0,0.3)', line_color: 'rgba(0,0,0,0.3)',
line_color_invalid: 'rgba(255,51,51,0.6)', line_color_invalid: 'rgba(255,51,51,0.6)',
lookup_delay: 200, lookup_delay: 200,
lookup_interval: 100, lookup_interval: 100,
scrolling_trigger_width: 20, scrolling_trigger_width: 20,
scrolling_step_length: 10, scrolling_step_length: 10,
shadow_node_class_name: 'jsmind-draggable-shadow-node', shadow_node_class_name: 'jsmind-draggable-shadow-node',
} }
class DraggableNode { class DraggableNode {
constructor(jm, options) { constructor(jm, options) {
var opts = {} var opts = {}
jsMind.util.json.merge(opts, DEFAULT_OPTIONS) jsMind.util.json.merge(opts, DEFAULT_OPTIONS)
jsMind.util.json.merge(opts, options) jsMind.util.json.merge(opts, options)
this.version = '0.4.0' this.version = '0.4.0'
this.jm = jm this.jm = jm
this.options = opts this.options = opts
this.e_canvas = null this.e_canvas = null
this.canvas_ctx = null this.canvas_ctx = null
this.shadow = null this.shadow = null
this.shadow_p_x = 0 this.shadow_p_x = 0
this.shadow_p_y = 0 this.shadow_p_y = 0
this.shadow_w = 0 this.shadow_w = 0
this.shadow_h = 0 this.shadow_h = 0
this.active_node = null this.active_node = null
this.target_node = null this.target_node = null
this.target_direct = null this.target_direct = null
this.client_w = 0 this.client_w = 0
this.client_h = 0 this.client_h = 0
this.offset_x = 0 this.offset_x = 0
this.offset_y = 0 this.offset_y = 0
this.hlookup_delay = 0 this.hlookup_delay = 0
this.hlookup_timer = 0 this.hlookup_timer = 0
this.capture = false this.capture = false
this.moved = false this.moved = false
this.canvas_draggable = jm.get_view_draggable() this.canvas_draggable = jm.get_view_draggable()
this.view_panel = jm.view.e_panel this.view_panel = jm.view.e_panel
this.view_panel_rect = null this.view_panel_rect = null
} }
init() { init() {
this.create_canvas() this.create_canvas()
this.create_shadow() this.create_shadow()
this.event_bind() this.event_bind()
} }
resize() { resize() {
this.jm.view.e_nodes.appendChild(this.shadow) this.jm.view.e_nodes.appendChild(this.shadow)
this.e_canvas.width = this.jm.view.size.w this.e_canvas.width = this.jm.view.size.w
this.e_canvas.height = this.jm.view.size.h this.e_canvas.height = this.jm.view.size.h
} }
create_canvas() { create_canvas() {
var c = $.c('canvas') var c = $.c('canvas')
this.jm.view.e_panel.appendChild(c) this.jm.view.e_panel.appendChild(c)
var ctx = c.getContext('2d') var ctx = c.getContext('2d')
this.e_canvas = c this.e_canvas = c
this.canvas_ctx = ctx this.canvas_ctx = ctx
} }
create_shadow() { create_shadow() {
var s = $.c('jmnode') var s = $.c('jmnode')
s.style.visibility = 'hidden' s.style.visibility = 'hidden'
s.style.zIndex = '3' s.style.zIndex = '3'
s.style.cursor = 'move' s.style.cursor = 'move'
s.style.opacity = '0.7' s.style.opacity = '0.7'
s.className = this.options.shadow_node_class_name s.className = this.options.shadow_node_class_name
this.shadow = s this.shadow = s
} }
reset_shadow(el) { reset_shadow(el) {
var s = this.shadow.style var s = this.shadow.style
this.shadow.innerHTML = el.innerHTML this.shadow.innerHTML = el.innerHTML
s.left = el.style.left s.left = el.style.left
s.top = el.style.top s.top = el.style.top
s.width = el.style.width s.width = el.style.width
s.height = el.style.height s.height = el.style.height
s.backgroundImage = el.style.backgroundImage s.backgroundImage = el.style.backgroundImage
s.backgroundSize = el.style.backgroundSize s.backgroundSize = el.style.backgroundSize
s.transform = el.style.transform s.transform = el.style.transform
this.shadow_w = this.shadow.clientWidth this.shadow_w = this.shadow.clientWidth
this.shadow_h = this.shadow.clientHeight this.shadow_h = this.shadow.clientHeight
} }
show_shadow() { show_shadow() {
if (!this.moved) { if (!this.moved) {
this.shadow.style.visibility = 'visible' this.shadow.style.visibility = 'visible'
} }
} }
hide_shadow() { hide_shadow() {
this.shadow.style.visibility = 'hidden' this.shadow.style.visibility = 'hidden'
} }
magnet_shadow(shadow_p, node_p, invalid) { magnet_shadow(shadow_p, node_p, invalid) {
this.canvas_ctx.lineWidth = this.options.line_width this.canvas_ctx.lineWidth = this.options.line_width
this.canvas_ctx.strokeStyle = invalid this.canvas_ctx.strokeStyle = invalid
? this.options.line_color_invalid ? this.options.line_color_invalid
: this.options.line_color : this.options.line_color
this.canvas_ctx.lineCap = 'round' this.canvas_ctx.lineCap = 'round'
this.clear_lines() this.clear_lines()
this.canvas_lineto(shadow_p.x, shadow_p.y, node_p.x, node_p.y) this.canvas_lineto(shadow_p.x, shadow_p.y, node_p.x, node_p.y)
} }
clear_lines() { clear_lines() {
this.canvas_ctx.clearRect(0, 0, this.jm.view.size.w, this.jm.view.size.h) this.canvas_ctx.clearRect(0, 0, this.jm.view.size.w, this.jm.view.size.h)
} }
canvas_lineto(x1, y1, x2, y2) { canvas_lineto(x1, y1, x2, y2) {
this.canvas_ctx.beginPath() this.canvas_ctx.beginPath()
this.canvas_ctx.moveTo(x1, y1) this.canvas_ctx.moveTo(x1, y1)
this.canvas_ctx.lineTo(x2, y2) this.canvas_ctx.lineTo(x2, y2)
this.canvas_ctx.stroke() this.canvas_ctx.stroke()
} }
event_bind() { event_bind() {
var jd = this var jd = this
var container = this.jm.view.container var container = this.jm.view.container
$.on(container, 'mousedown', function (e) { $.on(container, 'mousedown', function (e) {
if (e.button === 0) { if (e.button === 0) {
jd.dragstart.call(jd, e) jd.dragstart.call(jd, e)
} }
}) })
$.on(container, 'mousemove', function (e) { $.on(container, 'mousemove', function (e) {
if (e.movementX !== 0 || e.movementY !== 0) { if (e.movementX !== 0 || e.movementY !== 0) {
jd.drag.call(jd, e) jd.drag.call(jd, e)
} }
}) })
$.on(container, 'mouseup', function (e) { $.on(container, 'mouseup', function (e) {
jd.dragend.call(jd, e) jd.dragend.call(jd, e)
}) })
$.on(container, 'touchstart', function (e) { $.on(container, 'touchstart', function (e) {
jd.dragstart.call(jd, e) jd.dragstart.call(jd, e)
}) })
$.on(container, 'touchmove', function (e) { $.on(container, 'touchmove', function (e) {
jd.drag.call(jd, e) jd.drag.call(jd, e)
}) })
$.on(container, 'touchend', function (e) { $.on(container, 'touchend', function (e) {
jd.dragend.call(jd, e) jd.dragend.call(jd, e)
}) })
} }
dragstart(e) { dragstart(e) {
if (!this.jm.get_editable()) { if (!this.jm.get_editable()) {
return return
} }
if (this.capture) { if (this.capture) {
return return
} }
var jview = this.jm.view var jview = this.jm.view
if (jview.is_editing()) { if (jview.is_editing()) {
return return
} }
this.active_node = null this.active_node = null
this.view_draggable = this.jm.get_view_draggable() this.view_draggable = this.jm.get_view_draggable()
var el = this.find_node_element(e.target) var el = this.find_node_element(e.target)
if (!el) { if (!el) {
return return
} }
if (this.view_draggable) { if (this.view_draggable) {
this.jm.disable_view_draggable() this.jm.disable_view_draggable()
} }
var nodeid = jview.get_binded_nodeid(el) var nodeid = jview.get_binded_nodeid(el)
if (!!nodeid) { if (!!nodeid) {
var node = this.jm.get_node(nodeid) var node = this.jm.get_node(nodeid)
if (!node.isroot) { if (!node.isroot) {
this.reset_shadow(el) this.reset_shadow(el)
this.view_panel_rect = this.view_panel.getBoundingClientRect() this.view_panel_rect = this.view_panel.getBoundingClientRect()
this.active_node = node this.active_node = node
this.offset_x = this.offset_x =
(e.clientX || e.touches[0].clientX) / jview.zoom_current - el.offsetLeft (e.clientX || e.touches[0].clientX) / jview.zoom_current - el.offsetLeft
this.offset_y = this.offset_y =
(e.clientY || e.touches[0].clientY) / jview.zoom_current - el.offsetTop (e.clientY || e.touches[0].clientY) / jview.zoom_current - el.offsetTop
this.client_hw = Math.floor(el.clientWidth / 2) this.client_hw = Math.floor(el.clientWidth / 2)
this.client_hh = Math.floor(el.clientHeight / 2) this.client_hh = Math.floor(el.clientHeight / 2)
if (this.hlookup_delay != 0) { if (this.hlookup_delay != 0) {
$.w.clearTimeout(this.hlookup_delay) $.w.clearTimeout(this.hlookup_delay)
} }
if (this.hlookup_timer != 0) { if (this.hlookup_timer != 0) {
$.w.clearInterval(this.hlookup_timer) $.w.clearInterval(this.hlookup_timer)
} }
var jd = this var jd = this
this.hlookup_delay = $.w.setTimeout(function () { this.hlookup_delay = $.w.setTimeout(function () {
jd.hlookup_delay = 0 jd.hlookup_delay = 0
jd.hlookup_timer = $.w.setInterval(function () { jd.hlookup_timer = $.w.setInterval(function () {
jd.lookup_target_node.call(jd) jd.lookup_target_node.call(jd)
}, jd.options.lookup_interval) }, jd.options.lookup_interval)
}, this.options.lookup_delay) }, this.options.lookup_delay)
jd.capture = true jd.capture = true
} }
} }
} }
drag(e) { drag(e) {
if (!this.jm.get_editable()) { if (!this.jm.get_editable()) {
return return
} }
if (this.capture) { if (this.capture) {
e.preventDefault() e.preventDefault()
this.show_shadow() this.show_shadow()
this.moved = true this.moved = true
clear_selection() clear_selection()
var jview = this.jm.view var jview = this.jm.view
var px = (e.clientX || e.touches[0].clientX) / jview.zoom_current - this.offset_x 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 var py = (e.clientY || e.touches[0].clientY) / jview.zoom_current - this.offset_y
// scrolling container axisY if drag nodes exceeding container // scrolling container axisY if drag nodes exceeding container
if ( if (
e.clientY - this.view_panel_rect.top < this.options.scrolling_trigger_width && e.clientY - this.view_panel_rect.top < this.options.scrolling_trigger_width &&
this.view_panel.scrollTop > this.options.scrolling_step_length this.view_panel.scrollTop > this.options.scrolling_step_length
) { ) {
this.view_panel.scrollBy(0, -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 this.offset_y += this.options.scrolling_step_length / jview.zoom_current
} else if ( } else if (
this.view_panel_rect.bottom - e.clientY < this.options.scrolling_trigger_width && this.view_panel_rect.bottom - e.clientY < this.options.scrolling_trigger_width &&
this.view_panel.scrollTop < this.view_panel.scrollTop <
this.view_panel.scrollHeight - this.view_panel.scrollHeight -
this.view_panel_rect.height - this.view_panel_rect.height -
this.options.scrolling_step_length this.options.scrolling_step_length
) { ) {
this.view_panel.scrollBy(0, 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 this.offset_y -= this.options.scrolling_step_length / jview.zoom_current
} }
// scrolling container axisX if drag nodes exceeding container // scrolling container axisX if drag nodes exceeding container
if ( if (
e.clientX - this.view_panel_rect.left < this.options.scrolling_trigger_width && e.clientX - this.view_panel_rect.left < this.options.scrolling_trigger_width &&
this.view_panel.scrollLeft > this.options.scrolling_step_length this.view_panel.scrollLeft > this.options.scrolling_step_length
) { ) {
this.view_panel.scrollBy(-this.options.scrolling_step_length, 0) this.view_panel.scrollBy(-this.options.scrolling_step_length, 0)
this.offset_x += this.options.scrolling_step_length / jview.zoom_current this.offset_x += this.options.scrolling_step_length / jview.zoom_current
} else if ( } else if (
this.view_panel_rect.right - e.clientX < this.options.scrolling_trigger_width && this.view_panel_rect.right - e.clientX < this.options.scrolling_trigger_width &&
this.view_panel.scrollLeft < this.view_panel.scrollLeft <
this.view_panel.scrollWidth - this.view_panel.scrollWidth -
this.view_panel_rect.width - this.view_panel_rect.width -
this.options.scrolling_step_length this.options.scrolling_step_length
) { ) {
this.view_panel.scrollBy(this.options.scrolling_step_length, 0) this.view_panel.scrollBy(this.options.scrolling_step_length, 0)
this.offset_x -= this.options.scrolling_step_length / jview.zoom_current this.offset_x -= this.options.scrolling_step_length / jview.zoom_current
} }
this.shadow.style.left = px + 'px' this.shadow.style.left = px + 'px'
this.shadow.style.top = py + 'px' this.shadow.style.top = py + 'px'
clear_selection() clear_selection()
} }
} }
dragend(e) { dragend(e) {
if (!this.jm.get_editable()) { if (!this.jm.get_editable()) {
return return
} }
if (this.view_draggable) { if (this.view_draggable) {
this.jm.enable_view_draggable() this.jm.enable_view_draggable()
} }
if (this.capture) { if (this.capture) {
if (this.hlookup_delay != 0) { if (this.hlookup_delay != 0) {
$.w.clearTimeout(this.hlookup_delay) $.w.clearTimeout(this.hlookup_delay)
this.hlookup_delay = 0 this.hlookup_delay = 0
this.clear_lines() this.clear_lines()
} }
if (this.hlookup_timer != 0) { if (this.hlookup_timer != 0) {
$.w.clearInterval(this.hlookup_timer) $.w.clearInterval(this.hlookup_timer)
this.hlookup_timer = 0 this.hlookup_timer = 0
this.clear_lines() this.clear_lines()
} }
if (this.moved) { if (this.moved) {
var src_node = this.active_node var src_node = this.active_node
var target_node = this.target_node var target_node = this.target_node
var target_direct = this.target_direct var target_direct = this.target_direct
this.move_node(src_node, target_node, target_direct) this.move_node(src_node, target_node, target_direct)
} }
this.hide_shadow() this.hide_shadow()
} }
this.view_panel_rect = null this.view_panel_rect = null
this.moved = false this.moved = false
this.capture = false this.capture = false
} }
find_node_element(el) { find_node_element(el) {
if ( if (
!el || !el ||
el === this.jm.view.e_nodes || el === this.jm.view.e_nodes ||
el === this.jm.view.e_panel || el === this.jm.view.e_panel ||
el === this.jm.view.container el === this.jm.view.container
) { ) {
return null return null
} }
if (el.tagName.toLowerCase() === 'jmnode') { if (el.tagName.toLowerCase() === 'jmnode') {
return el return el
} }
return this.find_node_element(el.parentNode) return this.find_node_element(el.parentNode)
} }
lookup_target_node() { lookup_target_node() {
let sx = this.shadow.offsetLeft let sx = this.shadow.offsetLeft
let sy = this.shadow.offsetTop let sy = this.shadow.offsetTop
if (sx === this.shadow_p_x && sy === this.shadow_p_y) { if (sx === this.shadow_p_x && sy === this.shadow_p_y) {
return return
} }
this.shadow_p_x = sx this.shadow_p_x = sx
this.shadow_p_y = sy this.shadow_p_y = sy
let target_direction = let target_direction =
this.shadow_p_x + this.shadow_w / 2 >= this.get_root_x() this.shadow_p_x + this.shadow_w / 2 >= this.get_root_x()
? jsMind.direction.right ? jsMind.direction.right
: jsMind.direction.left : jsMind.direction.left
let overlapping_node = this.lookup_overlapping_node_parent(target_direction) let overlapping_node = this.lookup_overlapping_node_parent(target_direction)
let target_node = overlapping_node || this.lookup_close_node(target_direction) let target_node = overlapping_node || this.lookup_close_node(target_direction)
if (!!target_node) { if (!!target_node) {
let points = this.calc_point_of_node(target_node, target_direction) let points = this.calc_point_of_node(target_node, target_direction)
let invalid = jsMind.node.inherited(this.active_node, target_node) let invalid = jsMind.node.inherited(this.active_node, target_node)
this.magnet_shadow(points.sp, points.np, invalid) this.magnet_shadow(points.sp, points.np, invalid)
this.target_node = target_node this.target_node = target_node
this.target_direct = target_direction this.target_direct = target_direction
} }
} }
get_root_x() { get_root_x() {
let root = this.jm.get_root() let root = this.jm.get_root()
let root_location = root.get_location() let root_location = root.get_location()
let root_size = root.get_size() let root_size = root.get_size()
return root_location.x + root_size.w / 2 return root_location.x + root_size.w / 2
} }
lookup_overlapping_node_parent(direction) { lookup_overlapping_node_parent(direction) {
let shadowRect = this.shadow.getBoundingClientRect() let shadowRect = this.shadow.getBoundingClientRect()
let x = shadowRect.x + (shadowRect.width * (1 - direction)) / 2 let x = shadowRect.x + (shadowRect.width * (1 - direction)) / 2
let deltaX = (this.jm.options.layout.hspace + this.jm.options.layout.pspace) * direction let deltaX = (this.jm.options.layout.hspace + this.jm.options.layout.pspace) * direction
let deltaY = shadowRect.height let deltaY = shadowRect.height
let points = [ let points = [
[x, shadowRect.y], [x, shadowRect.y],
[x, shadowRect.y + deltaY / 2], [x, shadowRect.y + deltaY / 2],
[x, shadowRect.y + deltaY], [x, shadowRect.y + deltaY],
[x + deltaX / 2, shadowRect.y], [x + deltaX / 2, shadowRect.y],
[x + deltaX / 2, shadowRect.y + deltaY / 2], [x + deltaX / 2, shadowRect.y + deltaY / 2],
[x + deltaX / 2, shadowRect.y + deltaY], [x + deltaX / 2, shadowRect.y + deltaY],
[x + deltaX, shadowRect.y], [x + deltaX, shadowRect.y],
[x + deltaX, shadowRect.y + deltaY / 2], [x + deltaX, shadowRect.y + deltaY / 2],
[x + deltaX, shadowRect.y + deltaY], [x + deltaX, shadowRect.y + deltaY],
] ]
for (const p of points) { for (const p of points) {
let n = this.lookup_node_parent_by_location(p[0], p[1]) let n = this.lookup_node_parent_by_location(p[0], p[1])
if (!!n) { if (!!n) {
return n return n
} }
} }
} }
lookup_node_parent_by_location(x, y) { lookup_node_parent_by_location(x, y) {
return $.d return $.d
.elementsFromPoint(x, y) .elementsFromPoint(x, y)
.filter( .filter(
(x) => x.tagName === 'JMNODE' && x.className !== this.options.shadow_node_class_name (x) => x.tagName === 'JMNODE' && x.className !== this.options.shadow_node_class_name
) )
.map((el) => this.jm.view.get_binded_nodeid(el)) .map((el) => this.jm.view.get_binded_nodeid(el))
.map((id) => id && this.jm.mind.nodes[id]) .map((id) => id && this.jm.mind.nodes[id])
.map((n) => n && n.parent) .map((n) => n && n.parent)
.find((n) => n) .find((n) => n)
} }
lookup_close_node(direction) { lookup_close_node(direction) {
return Object.values(this.jm.mind.nodes) return Object.values(this.jm.mind.nodes)
.filter((n) => n.direction == direction || n.isroot) .filter((n) => n.direction == direction || n.isroot)
.filter((n) => this.jm.layout.is_visible(n)) .filter((n) => this.jm.layout.is_visible(n))
.filter((n) => this.shadow_on_target_side(n, direction)) .filter((n) => this.shadow_on_target_side(n, direction))
.map((n) => ({ node: n, distance: this.shadow_to_node(n, direction) })) .map((n) => ({ node: n, distance: this.shadow_to_node(n, direction) }))
.reduce( .reduce(
(prev, curr) => { (prev, curr) => {
return prev.distance < curr.distance ? prev : curr return prev.distance < curr.distance ? prev : curr
}, },
{ node: this.jm.get_root(), distance: Number.MAX_VALUE } { node: this.jm.get_root(), distance: Number.MAX_VALUE }
).node ).node
} }
shadow_on_target_side(node, dir) { shadow_on_target_side(node, dir) {
return ( return (
(dir == jsMind.direction.right && this.shadow_to_right_of_node(node) > 0) || (dir == jsMind.direction.right && this.shadow_to_right_of_node(node) > 0) ||
(dir == jsMind.direction.left && this.shadow_to_left_of_node(node) > 0) (dir == jsMind.direction.left && this.shadow_to_left_of_node(node) > 0)
) )
} }
shadow_to_right_of_node(node) { shadow_to_right_of_node(node) {
return this.shadow_p_x - node.get_location().x - node.get_size().w return this.shadow_p_x - node.get_location().x - node.get_size().w
} }
shadow_to_left_of_node(node) { shadow_to_left_of_node(node) {
return node.get_location().x - this.shadow_p_x - this.shadow_w return node.get_location().x - this.shadow_p_x - this.shadow_w
} }
shadow_to_base_line_of_node(node) { 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 return this.shadow_p_y + this.shadow_h / 2 - node.get_location().y - node.get_size().h / 2
} }
shadow_to_node(node, dir) { shadow_to_node(node, dir) {
let distance_x = let distance_x =
dir === jsMind.direction.right dir === jsMind.direction.right
? Math.abs(this.shadow_to_right_of_node(node)) ? Math.abs(this.shadow_to_right_of_node(node))
: Math.abs(this.shadow_to_left_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)) let distance_y = Math.abs(this.shadow_to_base_line_of_node(node))
return distance_x + distance_y return distance_x + distance_y
} }
calc_point_of_node(node, dir) { calc_point_of_node(node, dir) {
let ns = node.get_size() let ns = node.get_size()
let nl = node.get_location() let nl = node.get_location()
let node_x = node.isroot let node_x = node.isroot
? nl.x + ns.w / 2 ? nl.x + ns.w / 2
: nl.x + (ns.w * (1 + dir)) / 2 + this.options.line_width * dir : nl.x + (ns.w * (1 + dir)) / 2 + this.options.line_width * dir
let node_y = nl.y + ns.h / 2 let node_y = nl.y + ns.h / 2
let shadow_x = let shadow_x =
this.shadow_p_x + (this.shadow_w * (1 - dir)) / 2 - this.options.line_width * dir 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 let shadow_y = this.shadow_p_y + this.shadow_h / 2
return { return {
sp: { x: shadow_x, y: shadow_y }, sp: { x: shadow_x, y: shadow_y },
np: { x: node_x, y: node_y }, np: { x: node_x, y: node_y },
} }
} }
move_node(src_node, target_node, target_direct) { move_node(src_node, target_node, target_direct) {
var shadow_h = this.shadow.offsetTop var shadow_h = this.shadow.offsetTop
if (!!target_node && !!src_node && !jsMind.node.inherited(src_node, target_node)) { if (!!target_node && !!src_node && !jsMind.node.inherited(src_node, target_node)) {
// lookup before_node // lookup before_node
var sibling_nodes = target_node.children var sibling_nodes = target_node.children
var sc = sibling_nodes.length var sc = sibling_nodes.length
var node = null var node = null
var delta_y = Number.MAX_VALUE var delta_y = Number.MAX_VALUE
var node_before = null var node_before = null
var beforeid = '_last_' var beforeid = '_last_'
while (sc--) { while (sc--) {
node = sibling_nodes[sc] node = sibling_nodes[sc]
if (node.direction == target_direct && node.id != src_node.id) { if (node.direction == target_direct && node.id != src_node.id) {
var dy = node.get_location().y - shadow_h var dy = node.get_location().y - shadow_h
if (dy > 0 && dy < delta_y) { if (dy > 0 && dy < delta_y) {
delta_y = dy delta_y = dy
node_before = node node_before = node
beforeid = '_first_' beforeid = '_first_'
} }
} }
} }
if (!!node_before) { if (!!node_before) {
beforeid = node_before.id beforeid = node_before.id
} }
this.jm.move_node(src_node.id, beforeid, target_node.id, target_direct) this.jm.move_node(src_node.id, beforeid, target_node.id, target_direct)
} }
this.active_node = null this.active_node = null
this.target_node = null this.target_node = null
this.target_direct = null this.target_direct = null
} }
jm_event_handle(type, data) { jm_event_handle(type, data) {
if (type === jsMind.event_type.resize) { if (type === jsMind.event_type.resize) {
this.resize() this.resize()
} }
} }
} }
var draggable_plugin = new jsMind.plugin('draggable_node', function (jm, options) { var draggable_plugin = new jsMind.plugin('draggable_node', function (jm, options) {
var jd = new DraggableNode(jm, options) var jd = new DraggableNode(jm, options)
jd.init() jd.init()
jm.add_event_listener(function (type, data) { jm.add_event_listener(function (type, data) {
jd.jm_event_handle.call(jd, type, data) jd.jm_event_handle.call(jd, type, data)
}) })
}) })
jsMind.register_plugin(draggable_plugin) jsMind.register_plugin(draggable_plugin)

View File

@ -1,158 +1,158 @@
import jsMind from 'jsmind' import jsMind from 'jsmind'
import domtoimage from 'dom-to-image' import domtoimage from 'dom-to-image'
if (!jsMind) { if (!jsMind) {
throw new Error('jsMind is not defined') throw new Error('jsMind is not defined')
} }
if (!domtoimage) { if (!domtoimage) {
throw new Error('dom-to-image is required') throw new Error('dom-to-image is required')
} }
const $ = jsMind.$ const $ = jsMind.$
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {
filename: null, filename: null,
watermark: { watermark: {
left: $.w.location, left: $.w.location,
right: 'https://github.com/hizzgdev/jsmind', right: 'https://github.com/hizzgdev/jsmind',
}, },
background: 'transparent', background: 'transparent',
} }
class JmScreenshot { class JmScreenshot {
constructor(jm, options) { constructor(jm, options) {
var opts = {} var opts = {}
jsMind.util.json.merge(opts, DEFAULT_OPTIONS) jsMind.util.json.merge(opts, DEFAULT_OPTIONS)
jsMind.util.json.merge(opts, options) jsMind.util.json.merge(opts, options)
this.version = '0.2.0' this.version = '0.2.0'
this.jm = jm this.jm = jm
this.options = opts this.options = opts
this.dpr = jm.view.device_pixel_ratio this.dpr = jm.view.device_pixel_ratio
} }
shoot() { shoot() {
let c = this.create_canvas() let c = this.create_canvas()
let ctx = c.getContext('2d') let ctx = c.getContext('2d')
ctx.scale(this.dpr, this.dpr) ctx.scale(this.dpr, this.dpr)
Promise.resolve(ctx) Promise.resolve(ctx)
.then(() => this.draw_background(ctx)) .then(() => this.draw_background(ctx))
.then(() => this.draw_lines(ctx)) .then(() => this.draw_lines(ctx))
.then(() => this.draw_nodes(ctx)) .then(() => this.draw_nodes(ctx))
.then(() => this.draw_watermark(c, ctx)) .then(() => this.draw_watermark(c, ctx))
.then(() => this.download(c)) .then(() => this.download(c))
.then(() => this.clear(c)) .then(() => this.clear(c))
} }
create_canvas() { create_canvas() {
let c = $.c('canvas') let c = $.c('canvas')
const w = this.jm.view.size.w const w = this.jm.view.size.w
const h = this.jm.view.size.h const h = this.jm.view.size.h
c.width = w * this.dpr c.width = w * this.dpr
c.height = h * this.dpr c.height = h * this.dpr
c.style.width = w + 'px' c.style.width = w + 'px'
c.style.height = h + 'px' c.style.height = h + 'px'
c.style.visibility = 'hidden' c.style.visibility = 'hidden'
this.jm.view.e_panel.appendChild(c) this.jm.view.e_panel.appendChild(c)
return c return c
} }
clear(c) { clear(c) {
c.parentNode.removeChild(c) c.parentNode.removeChild(c)
} }
draw_background(ctx) { draw_background(ctx) {
return new Promise( return new Promise(
function (resolve, _) { function (resolve, _) {
const bg = this.options.background const bg = this.options.background
if (!!bg && bg !== 'transparent') { if (!!bg && bg !== 'transparent') {
ctx.fillStyle = this.options.background ctx.fillStyle = this.options.background
ctx.fillRect(0, 0, this.jm.view.size.w, this.jm.view.size.h) ctx.fillRect(0, 0, this.jm.view.size.w, this.jm.view.size.h)
} }
resolve(ctx) resolve(ctx)
}.bind(this) }.bind(this)
) )
} }
draw_lines(ctx) { draw_lines(ctx) {
return new Promise( return new Promise(
function (resolve, _) { function (resolve, _) {
this.jm.view.graph.copy_to(ctx, function () { this.jm.view.graph.copy_to(ctx, function () {
resolve(ctx) resolve(ctx)
}) })
}.bind(this) }.bind(this)
) )
} }
draw_nodes(ctx) { draw_nodes(ctx) {
return domtoimage return domtoimage
.toSvg(this.jm.view.e_nodes, { style: { zoom: 1 } }) .toSvg(this.jm.view.e_nodes, { style: { zoom: 1 } })
.then(this.load_image) .then(this.load_image)
.then(function (img) { .then(function (img) {
ctx.drawImage(img, 0, 0) ctx.drawImage(img, 0, 0)
return ctx return ctx
}) })
} }
draw_watermark(c, ctx) { draw_watermark(c, ctx) {
ctx.textBaseline = 'bottom' ctx.textBaseline = 'bottom'
ctx.fillStyle = '#000' ctx.fillStyle = '#000'
ctx.font = '11px Verdana,Arial,Helvetica,sans-serif' ctx.font = '11px Verdana,Arial,Helvetica,sans-serif'
if (!!this.options.watermark.left) { if (!!this.options.watermark.left) {
ctx.textAlign = 'left' ctx.textAlign = 'left'
ctx.fillText(this.options.watermark.left, 5.5, c.height - 2.5) ctx.fillText(this.options.watermark.left, 5.5, c.height - 2.5)
} }
if (!!this.options.watermark.right) { if (!!this.options.watermark.right) {
ctx.textAlign = 'right' ctx.textAlign = 'right'
ctx.fillText(this.options.watermark.right, c.width - 5.5, c.height - 2.5) ctx.fillText(this.options.watermark.right, c.width - 5.5, c.height - 2.5)
} }
return ctx return ctx
} }
load_image(url) { load_image(url) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
let img = new Image() let img = new Image()
img.onload = function () { img.onload = function () {
resolve(img) resolve(img)
} }
img.onerror = reject img.onerror = reject
img.src = url img.src = url
}) })
} }
download(c) { download(c) {
var name = (this.options.filename || this.jm.mind.name) + '.png' var name = (this.options.filename || this.jm.mind.name) + '.png'
if (navigator.msSaveBlob && !!c.msToBlob) { if (navigator.msSaveBlob && !!c.msToBlob) {
var blob = c.msToBlob() var blob = c.msToBlob()
navigator.msSaveBlob(blob, name) navigator.msSaveBlob(blob, name)
} else { } else {
var blob_url = c.toDataURL() var blob_url = c.toDataURL()
var anchor = $.c('a') var anchor = $.c('a')
if ('download' in anchor) { if ('download' in anchor) {
anchor.style.visibility = 'hidden' anchor.style.visibility = 'hidden'
anchor.href = blob_url anchor.href = blob_url
anchor.download = name anchor.download = name
$.d.body.appendChild(anchor) $.d.body.appendChild(anchor)
var evt = $.d.createEvent('MouseEvents') var evt = $.d.createEvent('MouseEvents')
evt.initEvent('click', true, true) evt.initEvent('click', true, true)
anchor.dispatchEvent(evt) anchor.dispatchEvent(evt)
$.d.body.removeChild(anchor) $.d.body.removeChild(anchor)
} else { } else {
location.href = blob_url location.href = blob_url
} }
} }
} }
} }
let screenshot_plugin = new jsMind.plugin('screenshot', function (jm, options) { let screenshot_plugin = new jsMind.plugin('screenshot', function (jm, options) {
var jmss = new JmScreenshot(jm, options) var jmss = new JmScreenshot(jm, options)
jm.screenshot = jmss jm.screenshot = jmss
jm.shoot = function () { jm.shoot = function () {
jmss.shoot() jmss.shoot()
} }
}) })
jsMind.register_plugin(screenshot_plugin) 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 // Color options management
export const PALETTE_COLORS = [ export const PALETTE_COLORS = [
'#c93a42', '#c93a42',
'#fbefdb', '#fbefdb',
'#0000FF', '#0000FF',
'#06c755', '#06c755',
'#FFBF48', '#FFBF48',
'#1e5b9e', '#1e5b9e',
'#000000', '#000000',
'#666666', '#666666',
'#999999', '#999999',
'#FFFFFF', '#FFFFFF',
// 紅色系Morandi Red // 紅色系Morandi Red
'#c8a39e', '#c8a39e',
'#b4746c', '#b4746c',
'#a16666', '#a16666',
'#d3a29d', '#d3a29d',
'#8e5d5a', '#8e5d5a',
// 橘色系Morandi Orange // 橘色系Morandi Orange
'#d4a186', '#d4a186',
'#e1b7a7', '#e1b7a7',
'#c98e63', '#c98e63',
'#e5b58e', '#e5b58e',
'#b57758', '#b57758',
// 黃色系Morandi Yellow // 黃色系Morandi Yellow
'#d8c29d', '#d8c29d',
'#e6d3aa', '#e6d3aa',
'#c4b07c', '#c4b07c',
'#e2c892', '#e2c892',
'#a8985c', '#a8985c',
// 綠色系Morandi Green // 綠色系Morandi Green
'#a3b1a8', '#a3b1a8',
'#8ca39b', '#8ca39b',
'#9fb7ad', '#9fb7ad',
'#b0c0ae', '#b0c0ae',
'#798d87', '#798d87',
// 藍色系Morandi Blue // 藍色系Morandi Blue
'#9ca8b8', '#9ca8b8',
'#a0b1c2', '#a0b1c2',
'#8193a8', '#8193a8',
'#6e7d91', '#6e7d91',
'#c0c8d2', '#c0c8d2',
'#b8a9c9', '#b8a9c9',
'#c1adc8', '#c1adc8',
'#a68ca9', '#a68ca9',
'#cabed4', '#cabed4',
'#8f799e', '#8f799e',
'#a0a0a0', '#a0a0a0',
'#bcbcbc', '#bcbcbc',
'#8c8c8c', '#8c8c8c',
'#747474', '#747474',
'#5e5e5e', '#5e5e5e',
] ]
// 心智圖初始數據 // 心智圖初始數據
export const INITIAL_MIND = { export const INITIAL_MIND = {
meta: {}, meta: {},
format: 'node_array', format: 'node_array',
data: [ data: [
{ {
id: 'root', id: 'root',
topic: 'FirstNode', topic: 'FirstNode',
expanded: true, expanded: true,
isroot: true, isroot: true,
}, },
], ],
} }
// 模擬打 API // 模擬打 API
// Simulate an API call to search based on the query // Simulate an API call to search based on the query
export async function mockSearchApi(query, tableUID) { export async function mockSearchApi(query, tableUID) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open("GET", '/admin/universal_tables/get_entries?uid=' + tableUID + "&q=" + query + "&links=true"); xhr.open("GET", '/admin/universal_tables/get_entries?uid=' + tableUID + "&q=" + query + "&links=true");
xhr.setRequestHeader("Accept", "application/json"); xhr.setRequestHeader("Accept", "application/json");
xhr.onload = function () { xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status >= 200 && xhr.status < 300) {
try { try {
const data = JSON.parse(xhr.responseText); const data = JSON.parse(xhr.responseText);
resolve(data); // success resolve(data); // success
} catch (e) { } catch (e) {
reject(new Error("Invalid JSON response")); reject(new Error("Invalid JSON response"));
} }
} else { } else {
reject(new Error(`Request failed with status ${xhr.status}`)); reject(new Error(`Request failed with status ${xhr.status}`));
} }
}; };
xhr.onerror = function () { xhr.onerror = function () {
reject(new Error("Network error")); reject(new Error("Network error"));
}; };
xhr.send(); 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 from '../jsmind/jsmind.js'
import '../jsmind/plugins/jsmind.draggable-node.js' import '../jsmind/plugins/jsmind.draggable-node.js'
import { JsmindSearch } from './custom.search.js' import { JsmindSearch } from './custom.search.js'
import { JsmindToolbar } from './custom.toolbar.js' import { JsmindToolbar } from './custom.toolbar.js'
import { mockSearchApi } from './custom.config.js' import { mockSearchApi } from './custom.config.js'
/** /**
* 初始化 jsMind 心智圖 * 初始化 jsMind 心智圖
* Initialize jsMind mind map * Initialize jsMind mind map
* @param {Object} mind - 心智圖的資料 (Mind map data) * @param {Object} mind - 心智圖的資料 (Mind map data)
* @param {Object} options - 配置選項 (Configuration options) * @param {Object} options - 配置選項 (Configuration options)
* @param {boolean} isEditable - 是否可編輯 (Is editable) * @param {boolean} isEditable - 是否可編輯 (Is editable)
* @returns {Object} - jsMind 實例 (jsMind instance) * @returns {Object} - jsMind 實例 (jsMind instance)
*/ */
export function initJsmind(mind, options, isEditable) { export function initJsmind(mind, options, isEditable) {
const container = document.getElementById(options.container) const container = document.getElementById(options.container)
container.innerHTML = '' container.innerHTML = ''
options.editable = isEditable options.editable = isEditable
const jm = new jsMind(options) const jm = new jsMind(options)
// 依據是否可編輯調整顯示為連結或文字 // 依據是否可編輯調整顯示為連結或文字
// Adjust display as a link or text based on editability // Adjust display as a link or text based on editability
const formattedData = mind.data.map((node) => { const formattedData = mind.data.map((node) => {
node.topic = node.topic =
!isEditable && node.link !isEditable && node.link
? `<a href="${node.link}" target="_blank">${node.text}</a>` ? `<a href="${node.link}" target="_blank">${node.text}</a>`
: node.text || node.topic : node.text || node.topic
return node return node
}) })
jm.show({ meta: mind.meta, format: 'node_array', data: formattedData }) jm.show({ meta: mind.meta, format: 'node_array', data: formattedData })
// 掛載附加模組(遠程搜尋 & 工具列) // 掛載附加模組(遠程搜尋 & 工具列)
// Attach additional modules (Remote search & Toolbar) // Attach additional modules (Remote search & Toolbar)
if (isEditable) { if (isEditable) {
new JsmindSearch(jm, mockSearchApi, options.tableUID); new JsmindSearch(jm, mockSearchApi, options.tableUID);
new JsmindToolbar(jm, options) new JsmindToolbar(jm, options)
} }
return jm return jm
} }
/** /**
* 獲取當前心智圖數據 * 獲取當前心智圖數據
* Get the current mind map data * Get the current mind map data
* @param {Object} jm - jsMind 實例 (jsMind instance) * @param {Object} jm - jsMind 實例 (jsMind instance)
* @returns {Object} - 心智圖數據 (Mind map data) * @returns {Object} - 心智圖數據 (Mind map data)
*/ */
export function getJsmindData(jm) { export function getJsmindData(jm) {
return jm.get_data('node_array') 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 { util } from '../jsmind/jsmind.util.js'
import { Mind } from '../jsmind/jsmind.mind.js' import { Mind } from '../jsmind/jsmind.mind.js'
import { ViewProvider } from '../jsmind/jsmind.view_provider.js' import { ViewProvider } from '../jsmind/jsmind.view_provider.js'
import jsMind from '../jsmind/jsmind.js' import jsMind from '../jsmind/jsmind.js'
ViewProvider.prototype.edit_node_end = function () { ViewProvider.prototype.edit_node_end = function () {
if (this.editing_node != null) { if (this.editing_node != null) {
var node = this.editing_node var node = this.editing_node
this.editing_node = null this.editing_node = null
var view_data = node._data.view var view_data = node._data.view
var element = view_data.element var element = view_data.element
// 客製化修改:顯示文字由 node.data 控制 // 客製化修改:顯示文字由 node.data 控制
// Customization: Display text is controlled by node.data // Customization: Display text is controlled by node.data
var topic = node.data.text var topic = node.data.text
element.style.zIndex = 'auto' element.style.zIndex = 'auto'
element.removeChild(this.e_editor) element.removeChild(this.e_editor)
if (util.text.is_empty(topic) || node.topic === topic) { if (util.text.is_empty(topic) || node.topic === topic) {
this.render_node(element, node) this.render_node(element, node)
} else { } else {
this.jm.update_node(node.id, topic) this.jm.update_node(node.id, topic)
} }
} }
this.e_panel.focus() this.e_panel.focus()
} }
ViewProvider.prototype.select_node = function (node) { ViewProvider.prototype.select_node = function (node) {
if (!!this.selected_node) { if (!!this.selected_node) {
var element = this.selected_node._data.view.element var element = this.selected_node._data.view.element
element.className = element.className.replace(/\s*selected\b/i, '') element.className = element.className.replace(/\s*selected\b/i, '')
this.restore_selected_node_custom_style(this.selected_node) this.restore_selected_node_custom_style(this.selected_node)
} }
if (!!node) { if (!!node) {
this.selected_node = node this.selected_node = node
node._data.view.element.className += ' selected' node._data.view.element.className += ' selected'
// 客製化修改:不清除自定義樣式 // 客製化修改:不清除自定義樣式
// Customization: Do not clear custom styles // Customization: Do not clear custom styles
// this.clear_selected_node_custom_style(node) // this.clear_selected_node_custom_style(node)
} }
} }
const originalAddNode = Mind.prototype.add_node const originalAddNode = Mind.prototype.add_node
Mind.prototype.add_node = function ( Mind.prototype.add_node = function (
parent_node, parent_node,
node_id, node_id,
topic, topic,
data = undefined, data = undefined,
direction, direction,
expanded, expanded,
idx idx
) { ) {
if (data == undefined) { if (data == undefined) {
data = {} data = {}
for (let style of [ for (let style of [
'leading-line-color', 'leading-line-color',
'background-color', 'background-color',
'foreground-color', 'foreground-color',
]) { ]) {
if (data[style] == undefined && parent_node.data?.[style]) { if (data[style] == undefined && parent_node.data?.[style]) {
data[style] = parent_node.data[style] data[style] = parent_node.data[style]
} }
} }
} }
arguments[3] = data arguments[3] = data
return originalAddNode.apply(this, arguments) return originalAddNode.apply(this, arguments)
} }
const originalMousedownHandle = jsMind.prototype.mousedown_handle const originalMousedownHandle = jsMind.prototype.mousedown_handle
jsMind.prototype.mousedown_handle = function (e) { jsMind.prototype.mousedown_handle = function (e) {
if (e.button !== 0) return if (e.button !== 0) return
return originalMousedownHandle.apply(this, arguments) 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' import { getRelativePosition } from './custom.util.js'
const EDITOR_CLASS = 'jsmind-editor' // jsmind class name const EDITOR_CLASS = 'jsmind-editor' // jsmind class name
const SUGGESTION_BOX_CLASS = 'jsmind-suggestions' const SUGGESTION_BOX_CLASS = 'jsmind-suggestions'
const SUGGESTION_ITEM_CLASS = 'suggestion-item' const SUGGESTION_ITEM_CLASS = 'suggestion-item'
/** /**
* jsMind 搜尋管理 * jsMind 搜尋管理
* jsMind Search Manager * jsMind Search Manager
*/ */
export class JsmindSearch { export class JsmindSearch {
/** /**
* 建構搜尋 * 建構搜尋
* Constructor for search * Constructor for search
* @param {Object} jm - jsMind 實例 (jsMind instance) * @param {Object} jm - jsMind 實例 (jsMind instance)
* @param {Function} searchAPI - 遠程搜尋 API 函式 (Remote search API function) * @param {Function} searchAPI - 遠程搜尋 API 函式 (Remote search API function)
* @param {string} tableUID * @param {string} tableUID
*/ */
constructor(jm, searchAPI, tableUID) { constructor(jm, searchAPI, tableUID) {
this.jm = jm this.jm = jm
this.searchAPI = searchAPI this.searchAPI = searchAPI
this.container = document.getElementById(jm.options.container) this.container = document.getElementById(jm.options.container)
this.suggestionBox = null this.suggestionBox = null
this.tableUID = tableUID this.tableUID = tableUID
// 新增記錄節點與事件 handler // 新增記錄節點與事件 handler
this.currentNode = null this.currentNode = null
this._keydownHandler = null this._keydownHandler = null
this._inputHandler = null this._inputHandler = null
this.init() this.init()
} }
/** /**
* 初始化搜尋事件 * 初始化搜尋事件
* Initialize search events * Initialize search events
*/ */
init() { init() {
// 確保不會重複綁定 dblclick 事件 // 確保不會重複綁定 dblclick 事件
// Ensure double-click event is not bound multiple times // Ensure double-click event is not bound multiple times
this.container.removeEventListener('dblclick', this.onDoubleClick) this.container.removeEventListener('dblclick', this.onDoubleClick)
this.container.addEventListener('dblclick', this.onDoubleClick.bind(this)) this.container.addEventListener('dblclick', this.onDoubleClick.bind(this))
} }
/** /**
* 處理雙擊事件以觸發搜尋 * 處理雙擊事件以觸發搜尋
* Handle double-click event to trigger search * Handle double-click event to trigger search
* @param {Event} e - 事件對象 (Event object) * @param {Event} e - 事件對象 (Event object)
*/ */
onDoubleClick(e) { onDoubleClick(e) {
// 非可編輯狀態不執行 // 非可編輯狀態不執行
// Ignore if not editable // Ignore if not editable
if (!this.jm.options.editable) return if (!this.jm.options.editable) return
const node = this.jm.get_selected_node() const node = this.jm.get_selected_node()
if (!node) return if (!node) return
// 避免影響原生編輯功能,稍後執行 // 避免影響原生編輯功能,稍後執行
// Prevent interfering with native edit mode // Prevent interfering with native edit mode
setTimeout(() => this.handleSearch(node), 100) setTimeout(() => this.handleSearch(node), 100)
} }
/** /**
* 開始處理搜尋 * 開始處理搜尋
* Start handling search * Start handling search
* @param {Object} node - 當前選中節點 (Selected node) * @param {Object} node - 當前選中節點 (Selected node)
*/ */
handleSearch(node) { handleSearch(node) {
const inputField = document.querySelector(`.${EDITOR_CLASS}`) const inputField = document.querySelector(`.${EDITOR_CLASS}`)
if (!inputField) return if (!inputField) return
// 記住目前的 node // 記住目前的 node
this.currentNode = node this.currentNode = node
// 清除之前的 handler // 清除之前的 handler
if (this._keydownHandler) inputField.removeEventListener('keydown', this._keydownHandler) if (this._keydownHandler) inputField.removeEventListener('keydown', this._keydownHandler)
if (this._inputHandler) inputField.removeEventListener('input', this._inputHandler) if (this._inputHandler) inputField.removeEventListener('input', this._inputHandler)
// 新綁定 handler // 新綁定 handler
this._keydownHandler = this.onKeyDown.bind(this) this._keydownHandler = this.onKeyDown.bind(this)
this._inputHandler = this.onInput.bind(this) this._inputHandler = this.onInput.bind(this)
inputField.addEventListener('keydown', this._keydownHandler) inputField.addEventListener('keydown', this._keydownHandler)
inputField.addEventListener('input', this._inputHandler) inputField.addEventListener('input', this._inputHandler)
} }
/** /**
* 處理 Enter 鍵完成輸入 * 處理 Enter 鍵完成輸入
* Handle Enter key to finalize input * Handle Enter key to finalize input
* @param {Object} node - 當前節點 * @param {Object} node - 當前節點
* @param {KeyboardEvent} e - 鍵盤事件 * @param {KeyboardEvent} e - 鍵盤事件
*/ */
onKeyDown(e) { onKeyDown(e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault() e.preventDefault()
const input = e.target.value.trim() const input = e.target.value.trim()
const node = this.currentNode const node = this.currentNode
if (input && node) { if (input && node) {
node.data.text = input node.data.text = input
this.jm.end_edit() this.jm.end_edit()
this.jm.update_node(node.id, input) this.jm.update_node(node.id, input)
if (this.suggestionBox) { if (this.suggestionBox) {
this.suggestionBox.style.display = 'none' this.suggestionBox.style.display = 'none'
} }
this.currentNode = null // 清除參考 this.currentNode = null // 清除參考
} }
} }
} }
/** /**
* 處理使用者輸入 * 處理使用者輸入
* Handle user input * Handle user input
* @param {Object} node - 當前選中節點 (Selected node) * @param {Object} node - 當前選中節點 (Selected node)
* @param {Event} e - 輸入事件 (Input event) * @param {Event} e - 輸入事件 (Input event)
*/ */
async onInput(e) { async onInput(e) {
const query = e.target.value.trim() const query = e.target.value.trim()
if (!query) return if (!query) return
await new Promise((resolve) => setTimeout(resolve, 500)) await new Promise((resolve) => setTimeout(resolve, 500))
try { try {
const results = await this.searchAPI(query, this.tableUID) const results = await this.searchAPI(query, this.tableUID)
this.showSuggestion(this.currentNode, e.target, results) this.showSuggestion(this.currentNode, e.target, results)
} catch (error) { } catch (error) {
console.error('搜尋 API 錯誤:', error) console.error('搜尋 API 錯誤:', error)
} }
} }
/** /**
* 顯示搜尋建議框 * 顯示搜尋建議框
* Show search suggestion box * Show search suggestion box
* @param {Object} node - 當前選中節點 (Selected node) * @param {Object} node - 當前選中節點 (Selected node)
* @param {HTMLElement} inputElement - 輸入框 (Input field) * @param {HTMLElement} inputElement - 輸入框 (Input field)
* @param {Array} results - 搜尋結果 (Search results) * @param {Array} results - 搜尋結果 (Search results)
*/ */
showSuggestion(node, inputElement, results) { showSuggestion(node, inputElement, results) {
const container = this.container const container = this.container
const nodeElement = inputElement.parentNode const nodeElement = inputElement.parentNode
if (!nodeElement) return if (!nodeElement) return
const { left, top, height } = getRelativePosition(nodeElement, container) const { left, top, height } = getRelativePosition(nodeElement, container)
this.suggestionBox = this.suggestionBox || this.createSuggestionBox() this.suggestionBox = this.suggestionBox || this.createSuggestionBox()
// 更新建議框內容 // 更新建議框內容
// Update suggestion box content // Update suggestion box content
this.suggestionBox.innerHTML = results this.suggestionBox.innerHTML = results
.map(item => { .map(item => {
const fieldHtml = item.fields.map(f => { const fieldHtml = item.fields.map(f => {
const txt = f.url const txt = f.url
? `<a href="${f.url}" target="_blank">${f.text}</a>` ? `<a href="${f.url}" target="_blank">${f.text}</a>`
: f.text; : f.text;
return `<div class="field-row"><strong>${f.title}:</strong> ${txt}</div>`; return `<div class="field-row"><strong>${f.title}:</strong> ${txt}</div>`;
}).join(""); }).join("");
return ` return `
<div class="suggestion-item" data-link="${item.link}"> <div class="suggestion-item" data-link="${item.link}">
${fieldHtml} ${fieldHtml}
</div> </div>
`; `;
}) })
.join('') .join('')
this.suggestionBox.style.left = `${left}px` this.suggestionBox.style.left = `${left}px`
this.suggestionBox.style.top = `${top + height}px` this.suggestionBox.style.top = `${top + height}px`
this.suggestionBox.style.display = 'block' this.suggestionBox.style.display = 'block'
// 綁定建議點擊事件 // 綁定建議點擊事件
// Bind suggestion click events // Bind suggestion click events
document.querySelectorAll(`.${SUGGESTION_ITEM_CLASS}`).forEach((item) => { document.querySelectorAll(`.${SUGGESTION_ITEM_CLASS}`).forEach((item) => {
item.removeEventListener('mousedown', this.onSuggestionClick) item.removeEventListener('mousedown', this.onSuggestionClick)
item.addEventListener('mousedown', this.onSuggestionClick.bind(this, node)) item.addEventListener('mousedown', this.onSuggestionClick.bind(this, node))
}) })
} }
/** /**
* 建立搜尋建議框 * 建立搜尋建議框
* Create search suggestion box * Create search suggestion box
* @returns {HTMLElement} - 建議框 DOM (Suggestion box DOM) * @returns {HTMLElement} - 建議框 DOM (Suggestion box DOM)
*/ */
createSuggestionBox() { createSuggestionBox() {
let suggestionBox = document.getElementById(SUGGESTION_BOX_CLASS) let suggestionBox = document.getElementById(SUGGESTION_BOX_CLASS)
if (!suggestionBox) { if (!suggestionBox) {
suggestionBox = document.createElement('div') suggestionBox = document.createElement('div')
suggestionBox.classList.add(SUGGESTION_BOX_CLASS) suggestionBox.classList.add(SUGGESTION_BOX_CLASS)
this.container.appendChild(suggestionBox) this.container.appendChild(suggestionBox)
} }
return suggestionBox return suggestionBox
} }
/** /**
* 處理點擊建議 * 處理點擊建議
* Handle suggestion click * Handle suggestion click
* @param {Object} node - 當前選中節點 (Selected node) * @param {Object} node - 當前選中節點 (Selected node)
* @param {Event} e - 點擊事件 (Click event) * @param {Event} e - 點擊事件 (Click event)
*/ */
// onSuggestionClick(node, e) { // onSuggestionClick(node, e) {
// e.preventDefault() // e.preventDefault()
// const text = e.target.getAttribute('data-text') // const text = e.target.getAttribute('data-text')
// const link = e.target.getAttribute('data-link') // const link = e.target.getAttribute('data-link')
// node.data.text = text // node.data.text = text
// node.data.link = link // node.data.link = link
// this.jm.end_edit() // this.jm.end_edit()
// this.jm.update_node(node.id, text) // this.jm.update_node(node.id, text)
// // 選擇後隱藏建議框 // // 選擇後隱藏建議框
// // Hide suggestions after selection // // Hide suggestions after selection
// this.suggestionBox.style.display = 'none' // this.suggestionBox.style.display = 'none'
// } // }
onSuggestionClick(node, e) { onSuggestionClick(node, e) {
e.preventDefault() e.preventDefault()
const item = e.currentTarget // 確保抓到整個 .suggestion-item DIV const item = e.currentTarget // 確保抓到整個 .suggestion-item DIV
const html = item.innerHTML // 取得完整 HTML 當作 topic const html = item.innerHTML // 取得完整 HTML 當作 topic
node.data.text = html node.data.text = html
node.data.link = item.getAttribute('data-link') node.data.link = item.getAttribute('data-link')
this.jm.end_edit() this.jm.end_edit()
this.jm.update_node(node.id, html) this.jm.update_node(node.id, html)
this.suggestionBox.style.display = 'none' this.suggestionBox.style.display = 'none'
} }
} }
// ✅ 新增播放語音事件委派,支援動態插入的 voice-player // ✅ 新增播放語音事件委派,支援動態插入的 voice-player
let audio; let audio;
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
const target = e.target.closest('.voice-player'); const target = e.target.closest('.voice-player');
if (!target) return; if (!target) return;
e.preventDefault(); e.preventDefault();
let status = target.getAttribute('status'); let status = target.getAttribute('status');
if (audio) { if (audio) {
audio.pause(); audio.pause();
audio.currentTime = 0; audio.currentTime = 0;
} }
if (status === 'playing') { if (status === 'playing') {
target.setAttribute('status', ''); target.setAttribute('status', '');
const icon = target.querySelector('i'); const icon = target.querySelector('i');
icon?.classList.remove('fa-pause'); icon?.classList.remove('fa-pause');
icon?.classList.add('fa-play'); icon?.classList.add('fa-play');
} else { } else {
let mp3_url = target.getAttribute('data-content'); let mp3_url = target.getAttribute('data-content');
audio = new Audio(mp3_url); audio = new Audio(mp3_url);
audio.play(); audio.play();
target.setAttribute('status', 'playing'); target.setAttribute('status', 'playing');
const icon = target.querySelector('i'); const icon = target.querySelector('i');
icon?.classList.remove('fa-play'); icon?.classList.remove('fa-play');
icon?.classList.add('fa-pause'); icon?.classList.add('fa-pause');
audio.onended = function() { audio.onended = function() {
target.setAttribute('status', ''); target.setAttribute('status', '');
icon?.classList.remove('fa-pause'); icon?.classList.remove('fa-pause');
icon?.classList.add('fa-play'); 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 { util } from '../jsmind/jsmind.util.js'
import { getRelativePosition } from './custom.util.js' import { getRelativePosition } from './custom.util.js'
import { PALETTE_COLORS } from './custom.config.js' import { PALETTE_COLORS } from './custom.config.js'
const TOOLBAR_ID = 'jsmind-toolbar' const TOOLBAR_ID = 'jsmind-toolbar'
/** /**
* jsMind 工具列管理 * jsMind 工具列管理
* jsMind Toolbar Manager * jsMind Toolbar Manager
*/ */
export class JsmindToolbar { export class JsmindToolbar {
/** /**
* 建構工具列 * 建構工具列
* Constructor for toolbar * Constructor for toolbar
* @param {Object} jm - jsMind 實例 (jsMind instance) * @param {Object} jm - jsMind 實例 (jsMind instance)
* @param {Object} options - jsMind 實例 (options) * @param {Object} options - jsMind 實例 (options)
*/ */
constructor(jm, options) { constructor(jm, options) {
this.jm = jm this.jm = jm
this.container = document.getElementById(jm.options.container) this.container = document.getElementById(jm.options.container)
this.toolbarNodeId = null this.toolbarNodeId = null
this.toolbar = null this.toolbar = null
this.bgColorPalette = null this.bgColorPalette = null
this.strokeColorPalette = null this.strokeColorPalette = null
this.textColorPalette = null this.textColorPalette = null
this.options = options this.options = options
this.init() this.init()
} }
/** /**
* 初始化工具列事件 * 初始化工具列事件
* Initialize toolbar events * Initialize toolbar events
*/ */
init() { init() {
// 監聽節點選取事件 // 監聽節點選取事件
// Listen for node selection events // Listen for node selection events
this.jm.add_event_listener((e, f, g) => { this.jm.add_event_listener((e, f, g) => {
// 忽略非選擇節點事件 // 忽略非選擇節點事件
// Ignore non-selection events // Ignore non-selection events
if (e !== 4) return if (e !== 4) return
const node = this.jm.get_selected_node() const node = this.jm.get_selected_node()
if (!node || node.id === this.toolbarNodeId) return if (!node || node.id === this.toolbarNodeId) return
this.toolbarNodeId = node.id this.toolbarNodeId = node.id
if (!this.toolbar) { if (!this.toolbar) {
this.createToolbar() this.createToolbar()
} }
this.moveToolbar(node) this.moveToolbar(node)
}) })
// 確保不會重複綁定點擊事件 // 確保不會重複綁定點擊事件
// Ensure click event is not bound multiple times // Ensure click event is not bound multiple times
this.container.removeEventListener('click', this.onClickOutside) this.container.removeEventListener('click', this.onClickOutside)
this.container.addEventListener('click', this.onClickOutside.bind(this)) this.container.addEventListener('click', this.onClickOutside.bind(this))
} }
/** /**
* 處理點擊事件來隱藏工具列 * 處理點擊事件來隱藏工具列
* Handle click event to hide toolbar * Handle click event to hide toolbar
* @param {Event} e - 事件對象 (Event object) * @param {Event} e - 事件對象 (Event object)
*/ */
onClickOutside(e) { onClickOutside(e) {
const clickedNode = e.target.tagName === 'JMNODE' const clickedNode = e.target.tagName === 'JMNODE'
const clickedToolbar = e.target.closest(`#${TOOLBAR_ID}`) const clickedToolbar = e.target.closest(`#${TOOLBAR_ID}`)
if (!clickedNode && !clickedToolbar && this.toolbar) { if (!clickedNode && !clickedToolbar && this.toolbar) {
this.hideToolbar() this.hideToolbar()
} }
} }
/** /**
* 建立工具列 UI * 建立工具列 UI
* Create toolbar UI * Create toolbar UI
*/ */
createToolbar() { createToolbar() {
this.toolbar = document.createElement('div') this.toolbar = document.createElement('div')
this.toolbar.id = TOOLBAR_ID this.toolbar.id = TOOLBAR_ID
// 建立工具列按鈕 // 建立工具列按鈕
// Create toolbar buttons // Create toolbar buttons
const buttons = [ const buttons = [
{ id: 'toolbar-add-child-btn', text: this.options.text.addNode, onClick: this.handleAddChild.bind(this) }, { 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-delete-btn', text: this.options.text.deleteNode, onClick: this.handleDelete.bind(this) },
{ {
id: 'toolbar-stroke-color-btn', id: 'toolbar-stroke-color-btn',
text: this.options.text.strokeColor, text: this.options.text.strokeColor,
onClick: this.handleStrokeColor.bind(this), onClick: this.handleStrokeColor.bind(this),
}, },
{ {
id: 'toolbar-bg-color-btn', id: 'toolbar-bg-color-btn',
text: this.options.text.bgColor, text: this.options.text.bgColor,
onClick: this.handleBgColor.bind(this), onClick: this.handleBgColor.bind(this),
}, },
{ {
id: 'toolbar-text-color-btn', id: 'toolbar-text-color-btn',
text: this.options.text.textColor, text: this.options.text.textColor,
onClick: this.handleTextColor.bind(this), onClick: this.handleTextColor.bind(this),
}, },
] ]
buttons.forEach((button) => { buttons.forEach((button) => {
const btn = document.createElement('button') const btn = document.createElement('button')
btn.id = button.id btn.id = button.id
btn.innerText = button.text btn.innerText = button.text
btn.onclick = button.onClick btn.onclick = button.onClick
this.toolbar.appendChild(btn) this.toolbar.appendChild(btn)
// 附加顏色選單 // 附加顏色選單
// Append color palettes to corresponding buttons // Append color palettes to corresponding buttons
if (button.id === 'toolbar-bg-color-btn') { if (button.id === 'toolbar-bg-color-btn') {
this.bgColorPalette = this.createColorPalette( this.bgColorPalette = this.createColorPalette(
(color) => this.setNodeStyle('background-color', color), (color) => this.setNodeStyle('background-color', color),
btn btn
) )
} }
if (button.id === 'toolbar-stroke-color-btn') { if (button.id === 'toolbar-stroke-color-btn') {
this.strokeColorPalette = this.createColorPalette( this.strokeColorPalette = this.createColorPalette(
(color) => this.setNodeStyle('leading-line-color', color), (color) => this.setNodeStyle('leading-line-color', color),
btn btn
) )
} }
if (button.id === 'toolbar-text-color-btn') { if (button.id === 'toolbar-text-color-btn') {
this.textColorPalette = this.createColorPalette( this.textColorPalette = this.createColorPalette(
(color) => this.setNodeStyle('foreground-color', color), (color) => this.setNodeStyle('foreground-color', color),
btn btn
) )
} }
}) })
this.container.appendChild(this.toolbar) this.container.appendChild(this.toolbar)
} }
/** /**
* 移動工具列至選中節點 * 移動工具列至選中節點
* Move the toolbar to the selected node * Move the toolbar to the selected node
*/ */
moveToolbar(node) { moveToolbar(node) {
const nodeElement = node._data.view.element const nodeElement = node._data.view.element
if (!nodeElement) return if (!nodeElement) return
const { left, top } = getRelativePosition(nodeElement, this.container) const { left, top } = getRelativePosition(nodeElement, this.container)
this.toolbar.style.left = `${left}px` this.toolbar.style.left = `${left}px`
this.toolbar.style.top = `${top - 40}px` this.toolbar.style.top = `${top - 40}px`
this.toolbar.style.display = 'block' this.toolbar.style.display = 'block'
// 根節點則隱藏刪除與線條顏色按鈕 // 根節點則隱藏刪除與線條顏色按鈕
// Hide delete & stroke color buttons if the node is root // Hide delete & stroke color buttons if the node is root
const deleteBtn = this.toolbar.querySelector('#toolbar-delete-btn') const deleteBtn = this.toolbar.querySelector('#toolbar-delete-btn')
const strokeColorBtn = this.toolbar.querySelector('#toolbar-stroke-color-btn') const strokeColorBtn = this.toolbar.querySelector('#toolbar-stroke-color-btn')
if (node.id === 'root') { if (node.id === 'root') {
deleteBtn.style.display = 'none' deleteBtn.style.display = 'none'
strokeColorBtn.style.display = 'none' strokeColorBtn.style.display = 'none'
} else { } else {
deleteBtn.style.display = 'inline-block' deleteBtn.style.display = 'inline-block'
strokeColorBtn.style.display = 'inline-block' strokeColorBtn.style.display = 'inline-block'
} }
} }
/** /**
* 隱藏工具列 * 隱藏工具列
* Hide the toolbar * Hide the toolbar
*/ */
hideToolbar() { hideToolbar() {
this.toolbar.style.display = 'none' this.toolbar.style.display = 'none'
this.bgColorPalette.style.display = 'none' this.bgColorPalette.style.display = 'none'
this.strokeColorPalette.style.display = 'none' this.strokeColorPalette.style.display = 'none'
this.textColorPalette.style.display = 'none' this.textColorPalette.style.display = 'none'
this.toolbarNodeId = null this.toolbarNodeId = null
} }
/** /**
* 建立顏色選單 * 建立顏色選單
* Create color palette * Create color palette
*/ */
createColorPalette(onSelect, button) { createColorPalette(onSelect, button) {
const colorPalette = document.createElement('div') const colorPalette = document.createElement('div')
colorPalette.classList.add('toolbar-color-palette') colorPalette.classList.add('toolbar-color-palette')
colorPalette.style.display = 'none' colorPalette.style.display = 'none'
PALETTE_COLORS.forEach((color) => { PALETTE_COLORS.forEach((color) => {
const colorBox = document.createElement('div') const colorBox = document.createElement('div')
colorBox.classList.add('toolbar-color-palette-box') colorBox.classList.add('toolbar-color-palette-box')
colorBox.style.backgroundColor = color colorBox.style.backgroundColor = color
colorBox.onclick = () => { colorBox.onclick = () => {
onSelect(color) onSelect(color)
colorPalette.style.display = 'none' colorPalette.style.display = 'none'
} }
colorPalette.appendChild(colorBox) colorPalette.appendChild(colorBox)
}) })
button.appendChild(colorPalette) button.appendChild(colorPalette)
return colorPalette return colorPalette
} }
/** /**
* 設定節點樣式 * 設定節點樣式
* Set node style * Set node style
*/ */
setNodeStyle(style, color) { setNodeStyle(style, color) {
if (!this.toolbarNodeId) return if (!this.toolbarNodeId) return
const node = this.jm.get_node(this.toolbarNodeId) const node = this.jm.get_node(this.toolbarNodeId)
if (!node) return if (!node) return
node.data[style] = color node.data[style] = color
if (style === 'leading-line-color') this.jm.view.show_lines() if (style === 'leading-line-color') this.jm.view.show_lines()
else this.jm.view.restore_selected_node_custom_style(node) else this.jm.view.restore_selected_node_custom_style(node)
} }
/** /**
* 處理新增節點事件 * 處理新增節點事件
* Handle add child node event * Handle add child node event
*/ */
handleAddChild(e) { handleAddChild(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation() e.stopPropagation()
if (!this.toolbarNodeId) return if (!this.toolbarNodeId) return
const node = this.jm.get_node(this.toolbarNodeId) const node = this.jm.get_node(this.toolbarNodeId)
if (!node) return if (!node) return
const newNode = this.jm.add_node(node, util.uuid.newid(), 'NewNode') const newNode = this.jm.add_node(node, util.uuid.newid(), 'NewNode')
this.jm.select_node(newNode) this.jm.select_node(newNode)
} }
/** /**
* 處理刪除節點事件 * 處理刪除節點事件
* Handle delete node event * Handle delete node event
*/ */
handleDelete(e) { handleDelete(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation() e.stopPropagation()
if (!this.toolbarNodeId) return if (!this.toolbarNodeId) return
const node = this.jm.get_node(this.toolbarNodeId) const node = this.jm.get_node(this.toolbarNodeId)
if (!node) return if (!node) return
this.jm.remove_node(node) this.jm.remove_node(node)
this.hideToolbar() this.hideToolbar()
} }
/** /**
* 處理其他樣式設定事件 * 處理其他樣式設定事件
* Handle style setting event * Handle style setting event
*/ */
handleStrokeColor(e) { handleStrokeColor(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation() e.stopPropagation()
this.toggleColorPalette(this.strokeColorPalette) this.toggleColorPalette(this.strokeColorPalette)
} }
handleBgColor(e) { handleBgColor(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation() e.stopPropagation()
this.toggleColorPalette(this.bgColorPalette) this.toggleColorPalette(this.bgColorPalette)
} }
handleTextColor(e) { handleTextColor(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation() e.stopPropagation()
this.toggleColorPalette(this.textColorPalette) this.toggleColorPalette(this.textColorPalette)
} }
/** /**
* 顯示或隱藏顏色選單 * 顯示或隱藏顏色選單
* Toggle color palette display * Toggle color palette display
*/ */
toggleColorPalette(palette) { toggleColorPalette(palette) {
;[this.bgColorPalette, this.strokeColorPalette, this.textColorPalette].forEach((p) => { ;[this.bgColorPalette, this.strokeColorPalette, this.textColorPalette].forEach((p) => {
if (p !== palette) p.style.display = 'none' if (p !== palette) p.style.display = 'none'
}) })
palette.style.display = palette.style.display === 'block' ? 'none' : 'block' 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 * Get the relative position of an element within a given container
* @param {HTMLElement} element - 目標元素 (Target element) * @param {HTMLElement} element - 目標元素 (Target element)
* @param {HTMLElement} container - 參考容器 (Reference container) * @param {HTMLElement} container - 參考容器 (Reference container)
* @returns {Object} - { left, top, height } 位置資訊 (Position details) * @returns {Object} - { left, top, height } 位置資訊 (Position details)
*/ */
export function getRelativePosition(element, container) { export function getRelativePosition(element, container) {
let nodeRect = element.getBoundingClientRect() let nodeRect = element.getBoundingClientRect()
let containerRect = container.getBoundingClientRect() let containerRect = container.getBoundingClientRect()
return { return {
left: nodeRect.left - containerRect.left, left: nodeRect.left - containerRect.left,
top: nodeRect.top - containerRect.top, top: nodeRect.top - containerRect.top,
height: nodeRect.height, 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 // This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below. // listed below.
// //
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, // 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. // 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 // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. // compiled file.
// //
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives. // about supported directives.
// //
//= require_tree . //= 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) 基礎布局 (Layout)
============================ */ ============================ */
.jsmind-inner { .jsmind-inner {
position: relative; position: relative;
overflow: auto; overflow: auto;
width: 100%; width: 100%;
height: 100%; height: 100%;
outline: none; outline: none;
user-select: none; /* 防止文字選取 */ user-select: none; /* 防止文字選取 */
} }
.jsmind-inner canvas { .jsmind-inner canvas {
position: absolute; position: absolute;
} }
#jsmind_container { #jsmind_container {
position: relative; position: relative;
height: 800px; height: 800px;
border: 1px solid #ccc; border: 1px solid #ccc;
background: #f4f4f4; background: #f4f4f4;
} }
/* ============================ /* ============================
層級管理 (Z-index) 層級管理 (Z-index)
============================ */ ============================ */
svg.jsmind, svg.jsmind,
canvas.jsmind { canvas.jsmind {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
} }
jmnodes { jmnodes {
position: absolute; position: absolute;
z-index: 2; z-index: 2;
background-color: rgba(0, 0, 0, 0); /* 透明背景,確保可點擊 */ background-color: rgba(0, 0, 0, 0); /* 透明背景,確保可點擊 */
} }
jmnode { jmnode {
position: absolute; position: absolute;
cursor: default; cursor: default;
max-width: 400px; max-width: 400px;
} }
jmexpander { jmexpander {
position: absolute; position: absolute;
width: 11px; width: 11px;
height: 11px; height: 11px;
display: block; display: block;
overflow: hidden; overflow: hidden;
line-height: 12px; line-height: 12px;
font-size: 10px; font-size: 10px;
text-align: center; text-align: center;
border-radius: 6px; border-radius: 6px;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
cursor: pointer; cursor: pointer;
} }
/* ============================ /* ============================
文字溢出控制 (Overflow) 文字溢出控制 (Overflow)
============================ */ ============================ */
.jmnode-overflow-wrap jmnodes { .jmnode-overflow-wrap jmnodes {
min-width: 420px; min-width: 420px;
} }
.jmnode-overflow-hidden jmnode { .jmnode-overflow-hidden jmnode {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* ============================ /* ============================
預設主題 (Default Theme) 預設主題 (Default Theme)
============================ */ ============================ */
jmnode { jmnode {
padding: 10px; padding: 10px;
background-color: #fff; background-color: #fff;
color: #333; color: #333;
border-radius: 5px; border-radius: 5px;
box-shadow: 1px 1px 1px #666; box-shadow: 1px 1px 1px #666;
font: 1em/1.125 Verdana, Arial, Helvetica, sans-serif; font: 1em/1.125 Verdana, Arial, Helvetica, sans-serif;
} }
jmnode:hover { jmnode:hover {
box-shadow: 2px 2px 8px #000; box-shadow: 2px 2px 8px #000;
filter: brightness(95%); filter: brightness(95%);
a{ a{
color: unset!important; color: unset!important;
} }
} }
jmnode.selected { jmnode.selected {
box-shadow: 2px 2px 8px #000; box-shadow: 2px 2px 8px #000;
filter: brightness(90%); filter: brightness(90%);
} }
jmnode.root { jmnode.root {
font-size:1.2em; font-size:1.2em;
/* 展開/收合按鈕 */ /* 展開/收合按鈕 */
border-color: gray; border-color: gray;
} }
jmexpander:hover { jmexpander:hover {
border-color: #000; border-color: #000;
} }
/* ============================ /* ============================
響應式設計 (Responsive) 響應式設計 (Responsive)
============================ */ ============================ */
@media screen and (max-device-width: 1024px) { @media screen and (max-device-width: 1024px) {
jmnode { jmnode {
padding: 5px; padding: 5px;
border-radius: 3px; border-radius: 3px;
font-size: 1.2em; font-size: 1.2em;
} }
jmnode.root { jmnode.root {
/* font-size: 21px; */ /* font-size: 21px; */
font-size: 1.2em; font-size: 1.2em;
} }
} }
/* ============================ /* ============================
工具列樣式 (Toolbar Styles) 工具列樣式 (Toolbar Styles)
============================ */ ============================ */
#jsmind-toolbar { #jsmind-toolbar {
position: absolute; position: absolute;
z-index: 1000; z-index: 1000;
} }
/* 顏色選單 */ /* 顏色選單 */
.toolbar-color-palette { .toolbar-color-palette {
position: absolute; position: absolute;
bottom: 30px; bottom: 30px;
background: #ccc; background: #ccc;
border: 1px solid #ccc; border: 1px solid #ccc;
z-index: 1001; z-index: 1001;
min-width: 10em; min-width: 10em;
} }
.toolbar-color-palette-box { .toolbar-color-palette-box {
width: 20px; width: 20px;
height: 20px; height: 20px;
margin: 2px; margin: 2px;
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
} }
/* ============================ /* ============================
遠程搜尋下拉選單 (Search Dropdown) 遠程搜尋下拉選單 (Search Dropdown)
============================ */ ============================ */
.jsmind-suggestions { .jsmind-suggestions {
position: absolute; position: absolute;
height: fit-content; height: fit-content;
width: 200px; width: 200px;
background: white; background: white;
border: 1px solid #ccc; border: 1px solid #ccc;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000; z-index: 1000;
} }
.suggestion-item { .suggestion-item {
padding: 8px; padding: 8px;
cursor: pointer; cursor: pointer;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
} }
.suggestion-item:hover { .suggestion-item:hover {
background: #f0f0f0; background: #f0f0f0;
} }
.field-row{ .field-row{
.column_entry_files{ .column_entry_files{
margin-top: 1em; margin-top: 1em;
} }
a{ a{
color: unset; color: unset;
} }
strong{ strong{
display: none; display: none;
} }
&:nth-child(2){ &:nth-child(2){
a{ a{
font-weight: 500; font-weight: 500;
font-size: 1.5em; font-size: 1.5em;
} }
} }
&:nth-child(4){ &:nth-child(4){
display: none; 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 * This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below. * listed below.
* *
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, * 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. * 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 * 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 * 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 * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
* file per style scope. * file per style scope.
* *
*= require_tree . *= require_tree .
*= require_self *= require_self
*/ */

View File

@ -1,471 +1,471 @@
.main-forms .utable-heading-wrap { .main-forms .utable-heading-wrap {
margin-bottom: 8px; margin-bottom: 8px;
border-radius: 0; border-radius: 0;
padding: 20px; padding: 20px;
border-top-right-radius: 4px; border-top-right-radius: 4px;
border-top-left-radius: 4px; border-top-left-radius: 4px;
.control-group:last-child { .control-group:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.controls { .controls {
margin-left: 0; margin-left: 0;
} }
} }
.utable-heading-header { .utable-heading-header {
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
margin-bottom: 20px; margin-bottom: 20px;
padding-bottom: 10px; padding-bottom: 10px;
h4 { h4 {
font-family: 'Chivo'; font-family: 'Chivo';
line-height: 26px; line-height: 26px;
margin: 0; margin: 0;
} }
} }
.utable-content { .utable-content {
.attributes { .attributes {
padding: 20px; padding: 20px;
&:nth-child(even) { &:nth-child(even) {
background-color: #e8e8e8; background-color: #e8e8e8;
} }
&:nth-child(odd) { &:nth-child(odd) {
background-color: #fff; background-color: #fff;
} }
.draggable { .draggable {
cursor: move; cursor: move;
} }
.draggable i{ .draggable i{
cursor: move; cursor: move;
vertical-align: middle; vertical-align: middle;
} }
} }
} }
.attributes-checkbox { .attributes-checkbox {
margin-left: 8px; margin-left: 8px;
} }
.main-forms > h3 { .main-forms > h3 {
margin: 5px 0 10px; margin: 5px 0 10px;
color: #333; color: #333;
text-shadow: 0 1px 0 #ffffff; text-shadow: 0 1px 0 #ffffff;
font-family: 'Playfair Display SC', sans-serif; font-family: 'Playfair Display SC', sans-serif;
} }
.main-forms fieldset { .main-forms fieldset {
background-color: #FFFFFF; background-color: #FFFFFF;
border: 1px solid #EDEDED; border: 1px solid #EDEDED;
margin-bottom: 20px; margin-bottom: 20px;
-webkit-border-radius: 5px; -webkit-border-radius: 5px;
-moz-border-radius: 5px; -moz-border-radius: 5px;
border-radius: 5px; border-radius: 5px;
.select-holder{ .select-holder{
margin-left: 20px; margin-left: 20px;
display: inline-block; display: inline-block;
span { span {
margin: 0 5px; margin: 0 5px;
} }
} }
} }
.main-forms fieldset .input-area:after { .main-forms fieldset .input-area:after {
content: ""; content: "";
clear: both; clear: both;
display: block; display: block;
height: 0; height: 0;
visibility: hidden; visibility: hidden;
} }
.main-forms fieldset .input-area .nav-name { .main-forms fieldset .input-area .nav-name {
float: left; float: left;
width: 100px; width: 100px;
padding-top: 5px; padding-top: 5px;
text-align: right; text-align: right;
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
font-size: 14px; font-size: 14px;
font-weight: normal; font-weight: normal;
line-height: 20px; line-height: 20px;
} }
.main-forms fieldset .input-area .controls textarea { .main-forms fieldset .input-area .controls textarea {
max-width: 500px; max-width: 500px;
max-height: 300px; max-height: 300px;
min-height: 86px; min-height: 86px;
} }
.main-forms fieldset .input-area .controls textarea.cke_source { .main-forms fieldset .input-area .controls textarea.cke_source {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
} }
.main-forms fieldset .input-area .controls hr { .main-forms fieldset .input-area .controls hr {
margin: 5px 0 10px; margin: 5px 0 10px;
} }
.main-forms fieldset .input-area .controls h5 { .main-forms fieldset .input-area .controls h5 {
margin: 5px 0; margin: 5px 0;
} }
.main-forms fieldset .input-area .controls .file-link { .main-forms fieldset .input-area .controls .file-link {
margin-right: 10px; margin-right: 10px;
display: inline-block; display: inline-block;
width: 177px; width: 177px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.main-forms fieldset .input-area .controls .input-prepend { .main-forms fieldset .input-area .controls .input-prepend {
margin-bottom: 5px; margin-bottom: 5px;
} }
.main-forms fieldset .input-area .controls .input-prepend .btn-group { .main-forms fieldset .input-area .controls .input-prepend .btn-group {
padding: 4px 12px; padding: 4px 12px;
} }
.main-forms fieldset .input-area .controls .input-prepend .btn-group { .main-forms fieldset .input-area .controls .input-prepend .btn-group {
padding: 4px 12px; 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 .radio input[type="radio"],
.main-forms fieldset .input-area .controls .input-prepend .btn-group .checkbox input[type="checkbox"] { .main-forms fieldset .input-area .controls .input-prepend .btn-group .checkbox input[type="checkbox"] {
margin: 5px 5px 0 0; margin: 5px 5px 0 0;
} }
.main-forms fieldset .input-area .controls .input-prepend .btn-group li { .main-forms fieldset .input-area .controls .input-prepend .btn-group li {
text-align: left; text-align: left;
padding: 3px 10px; padding: 3px 10px;
} }
.main-forms fieldset .input-area .controls .input-prepend .btn-group li label { .main-forms fieldset .input-area .controls .input-prepend .btn-group li label {
padding-left: 5px; padding-left: 5px;
display: block; display: block;
} }
.main-forms fieldset .input-area .controls .exist .input-prepend .btn-group:hover .dropdown-menu, .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 { .main-forms fieldset .input-area .controls .add-target .input-prepend .btn-group:hover .dropdown-menu {
display: block; display: block;
} }
.main-forms fieldset .input-area .controls .exist .input-prepend, .main-forms fieldset .input-area .controls .exist .input-prepend,
.main-forms fieldset .input-area .controls .add-target .input-prepend { .main-forms fieldset .input-area .controls .add-target .input-prepend {
margin-bottom: 10px; margin-bottom: 10px;
display: inline-block; display: inline-block;
} }
.main-forms fieldset .input-area .controls .exist .fileupload-new { .main-forms fieldset .input-area .controls .exist .fileupload-new {
display: block; display: block;
} }
.main-forms fieldset .input-area .controls .exist .fileupload-new .input-prepend { .main-forms fieldset .input-area .controls .exist .fileupload-new .input-prepend {
display: inline-block; display: inline-block;
} }
.main-forms fieldset .input-area .controls .input-prepend a { .main-forms fieldset .input-area .controls .input-prepend a {
text-decoration: none; text-decoration: none;
color: #333333; color: #333333;
} }
.main-forms fieldset .input-area .controls .input-prepend .tab-content > .active { .main-forms fieldset .input-area .controls .input-prepend .tab-content > .active {
display: inline-block; display: inline-block;
} }
.main-forms fieldset .input-area .controls .add-btn { .main-forms fieldset .input-area .controls .add-btn {
margin: 3px 0; margin: 3px 0;
} }
.main-forms fieldset .input-area .fileupload { .main-forms fieldset .input-area .fileupload {
margin-right: 15px; margin-right: 15px;
margin-bottom: 0; margin-bottom: 0;
} }
.main-forms fieldset .input-area .datetimepick { .main-forms fieldset .input-area .datetimepick {
margin-right: 5px; margin-right: 5px;
margin-bottom: 5px; margin-bottom: 5px;
} }
.main-forms fieldset .input-area .datetimepick .add-on { .main-forms fieldset .input-area .datetimepick .add-on {
line-height: 24px; line-height: 24px;
cursor: pointer; cursor: pointer;
} }
.main-forms fieldset .input-area .language-area .input-content .mceLayout { .main-forms fieldset .input-area .language-area .input-content .mceLayout {
width: 100%!important; width: 100%!important;
} }
.main-forms fieldset .input-area .module-nav, .main-forms fieldset .input-area .module-nav,
.main-forms fieldset .input-area .language-nav { .main-forms fieldset .input-area .language-nav {
margin: 0 0 20px; margin: 0 0 20px;
padding: 0 0 15px 120px; padding: 0 0 15px 120px;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
} }
.main-forms fieldset .input-area .module-nav li, .main-forms fieldset .input-area .module-nav li,
.main-forms fieldset .input-area .language-nav li { .main-forms fieldset .input-area .language-nav li {
position: relative; position: relative;
} }
.main-forms fieldset .input-area .module-nav li.active:before, .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 .module-nav li.active:after,
.main-forms fieldset .input-area .language-nav li.active:before, .main-forms fieldset .input-area .language-nav li.active:before,
.main-forms fieldset .input-area .language-nav li.active:after { .main-forms fieldset .input-area .language-nav li.active:after {
display: block; display: block;
height: 0px; height: 0px;
width: 0px; width: 0px;
position: absolute; position: absolute;
bottom: -15px; bottom: -15px;
left: 50%; left: 50%;
margin-left: -5px; margin-left: -5px;
content: ""; content: "";
border-style: solid; border-style: solid;
border-width: 0 6px 6px 6px; border-width: 0 6px 6px 6px;
border-color: transparent transparent #EDEDED transparent; border-color: transparent transparent #EDEDED transparent;
z-index: 5 z-index: 5
} }
.main-forms fieldset .input-area .module-nav li.active:after { .main-forms fieldset .input-area .module-nav li.active:after {
display: none; display: none;
} }
.main-forms fieldset .input-area .language-nav li.active:after { .main-forms fieldset .input-area .language-nav li.active:after {
bottom: -16px; bottom: -16px;
margin-left: -4px; margin-left: -4px;
border-width: 0 5px 5px 5px; border-width: 0 5px 5px 5px;
border-color: transparent transparent #FFFFFF transparent; border-color: transparent transparent #FFFFFF transparent;
} }
.main-forms fieldset .input-area .module-nav { .main-forms fieldset .input-area .module-nav {
margin-bottom: 0; margin-bottom: 0;
border-bottom: none; border-bottom: none;
} }
.main-forms fieldset .input-area .language-area, .main-forms fieldset .input-area .language-area,
.main-forms fieldset .input-area .module-area { .main-forms fieldset .input-area .module-area {
overflow: visible; overflow: visible;
} }
.main-forms fieldset .input-area .module-area { .main-forms fieldset .input-area .module-area {
padding-top: 20px; padding-top: 20px;
margin-bottom: 40px; margin-bottom: 40px;
background-color: #EDEDED; background-color: #EDEDED;
border-radius: 5px; border-radius: 5px;
overflow: hidden; overflow: hidden;
} }
.main-forms fieldset .form-actions { .main-forms fieldset .form-actions {
padding-left: 200px; padding-left: 200px;
margin: 0px; margin: 0px;
-webkit-border-radius: 0 0 4px 4px; -webkit-border-radius: 0 0 4px 4px;
-moz-border-radius: 0 0 4px 4px; -moz-border-radius: 0 0 4px 4px;
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
} }
.main-forms fieldset .input-area .nav-scroll { .main-forms fieldset .input-area .nav-scroll {
margin-left: 120px; margin-left: 120px;
width: 800px; width: 800px;
position: relative; position: relative;
z-index: 1; z-index: 1;
overflow: hidden; overflow: hidden;
} }
.main-forms fieldset .input-area .nav-scroll .scroller { .main-forms fieldset .input-area .nav-scroll .scroller {
width: 1000px; width: 1000px;
height: 100%; height: 100%;
float: left; float: left;
padding: 0; padding: 0;
} }
.main-forms fieldset .input-area .controls[data-toggle^="buttons-"] label { .main-forms fieldset .input-area .controls[data-toggle^="buttons-"] label {
position: relative; position: relative;
margin: 0 0 5px; 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="radio"],
.main-forms fieldset .input-area .controls[data-toggle^="buttons-"] input[type="checkbox"] { .main-forms fieldset .input-area .controls[data-toggle^="buttons-"] input[type="checkbox"] {
margin-left: 0; margin-left: 0;
margin-top: 0; margin-top: 0;
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; display: block;
opacity: 0; opacity: 0;
} }
.main-forms fieldset .input-area .question { .main-forms fieldset .input-area .question {
margin-top: 5px; margin-top: 5px;
} }
/* User Role Forms */ /* User Role Forms */
#attributes-area.clickHere { #attributes-area.clickHere {
min-height: 150px; min-height: 150px;
position: relative; position: relative;
} }
#attributes-area.clickHere:before { #attributes-area.clickHere:before {
font-family: 'entypo'; font-family: 'entypo';
content: '\e0be'; content: '\e0be';
position: absolute; position: absolute;
font-size: 8em; font-size: 8em;
display: block; display: block;
bottom: 50px; bottom: 50px;
left: 175px; left: 175px;
color: #51a351; color: #51a351;
opacity: .4; opacity: .4;
} }
.main-forms .input-append .tab-content { .main-forms .input-append .tab-content {
display: inline-block; display: inline-block;
overflow: inherit; overflow: inherit;
} }
.main-forms .input-append .tab-content .active { .main-forms .input-append .tab-content .active {
display: inline-block; display: inline-block;
background-color: transparent; background-color: transparent;
} }
.main-forms .input-append .active { .main-forms .input-append .active {
border-color: #c5c5c5; border-color: #c5c5c5;
border-color: rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.25); 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 { .main-forms .input-append > .btn-group > .btn:first-child {
margin-left: -1px; margin-left: -1px;
-webkit-border-bottom-left-radius: 0px; -webkit-border-bottom-left-radius: 0px;
border-bottom-left-radius: 0px; border-bottom-left-radius: 0px;
-webkit-border-top-left-radius: 0px; -webkit-border-top-left-radius: 0px;
border-top-left-radius: 0px; border-top-left-radius: 0px;
-moz-border-radius-bottomleft: 0px; -moz-border-radius-bottomleft: 0px;
-moz-border-radius-topleft: 0px; -moz-border-radius-topleft: 0px;
} }
.main-forms .attributes { .main-forms .attributes {
padding-bottom: 20px; padding-bottom: 20px;
} }
.main-forms .attributes .tab-content { .main-forms .attributes .tab-content {
overflow: inherit; overflow: inherit;
} }
.main-forms .attributes.disabled label, .main-forms .attributes.disabled label,
.main-forms .attributes.disabled h4 { .main-forms .attributes.disabled h4 {
color: #e6e6e6; color: #e6e6e6;
} }
.main-forms .attributes-header { .main-forms .attributes-header {
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
margin-bottom: 20px; margin-bottom: 20px;
padding-bottom: 10px; padding-bottom: 10px;
} }
.main-forms .attributes-header .btn { .main-forms .attributes-header .btn {
margin-left: 5px; margin-left: 5px;
} }
.main-forms .attributes-header h4 { .main-forms .attributes-header h4 {
font-family: 'Chivo'; font-family: 'Chivo';
line-height: 26px; line-height: 26px;
margin: 0; margin: 0;
} }
.main-forms .attributes-header h4 b { .main-forms .attributes-header h4 b {
padding: 0 1px; padding: 0 1px;
border-style: dotted; border-style: dotted;
border-width: 0 2px; border-width: 0 2px;
border-color: #AAA; border-color: #AAA;
margin-right: 5px; margin-right: 5px;
cursor: move; cursor: move;
} }
.main-forms .attributes-header h4 i { .main-forms .attributes-header h4 i {
cursor: pointer; cursor: pointer;
} }
.main-forms .field-type { .main-forms .field-type {
background-color: #f5f5f5; background-color: #f5f5f5;
border-radius: 5px; border-radius: 5px;
margin-bottom: 20px; margin-bottom: 20px;
padding: 10px; padding: 10px;
} }
/* Responsive */ /* Responsive */
@media (max-width: 480px) { @media (max-width: 480px) {
.main-forms fieldset .input-area .nav-name { .main-forms fieldset .input-area .nav-name {
float: none; float: none;
width: auto; width: auto;
padding-top: 0; padding-top: 0;
text-align: left; text-align: left;
} }
.main-forms fieldset .input-area .module-area { .main-forms fieldset .input-area .module-area {
padding: 20px; padding: 20px;
} }
.main-forms fieldset .input-area .module-nav, .main-forms fieldset .input-area .module-nav,
.main-forms fieldset .input-area .language-nav { .main-forms fieldset .input-area .language-nav {
padding: 0 0 15px 0px; padding: 0 0 15px 0px;
} }
.main-forms fieldset .form-actions { .main-forms fieldset .form-actions {
padding-right: 20px; padding-right: 20px;
padding-left: 20px; padding-left: 20px;
} }
.main-forms fieldset .input-area .control-label { .main-forms fieldset .input-area .control-label {
width: auto; width: auto;
} }
.main-forms fieldset .input-area .controls { .main-forms fieldset .input-area .controls {
margin-left: 0; margin-left: 0;
} }
.main-forms fieldset .form-actions { .main-forms fieldset .form-actions {
padding-left: 20px; padding-left: 20px;
} }
} }
// Bootstrap override // Bootstrap override
// fixing datepicker appearing behind modal // fixing datepicker appearing behind modal
.ut-table { .ut-table {
.image-preview { .image-preview {
width: 100px; width: 100px;
} }
.image-expander { .image-expander {
position: relative; position: relative;
display: inline-block; display: inline-block;
&:hover { &:hover {
.image-large { .image-large {
opacity: 1; opacity: 1;
} }
} }
} }
.image-large { .image-large {
border-radius: 2px; border-radius: 2px;
-webkit-transition: .3s all ease-in-out; -webkit-transition: .3s all ease-in-out;
transition: .3s all ease-in-out; transition: .3s all ease-in-out;
opacity: 0; opacity: 0;
position: absolute; position: absolute;
left: calc(100% + 10px); left: calc(100% + 10px);
top: -10px; top: -10px;
width: 100%; width: 100%;
background-color: #fff; background-color: #fff;
padding: 10px; padding: 10px;
box-shadow: 0 0 5px 0 rgba(0,0,0,.1); box-shadow: 0 0 5px 0 rgba(0,0,0,.1);
} }
} }
.ut-control-group-col { .ut-control-group-col {
float: left; float: left;
width: 350px; width: 350px;
} }
.ut-control-group-col-right { .ut-control-group-col-right {
width: 190px; width: 190px;
.control-label { .control-label {
width: auto; width: auto;
margin-right: 10px; margin-right: 10px;
} }
.controls { .controls {
margin-left: 0; 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 class Admin::MindMapsController < OrbitAdminController
def index def index
@table_fields = ["universal_table.mind_map","universal_table.created_time"] @table_fields = ["universal_table.mind_map","universal_table.created_time"]
@table = UTable.find(params[:id]) @table = UTable.find(params[:id])
@mind_maps = Kaminari.paginate_array(@table.mind_maps).page(params[:page]).per(10) @mind_maps = Kaminari.paginate_array(@table.mind_maps).page(params[:page]).per(10)
end end
def new def new
@table = UTable.find(params[:table]) @table = UTable.find(params[:table])
@mind_map = MindMap.new @mind_map = MindMap.new
end end
def edit def edit
uid = params[:id].split("-").last uid = params[:id].split("-").last
@mind_map = MindMap.where(:uid => uid).first @mind_map = MindMap.where(:uid => uid).first
@table = @mind_map.u_table @table = @mind_map.u_table
end end
def create def create
mind_map = MindMap.new mind_map = MindMap.new
mind_params = mind_map_params mind_params = mind_map_params
mind_params[:mind_map_data] = JSON.parse(mind_params[:mind_map_data]) mind_params[:mind_map_data] = JSON.parse(mind_params[:mind_map_data])
mind_map.update_attributes(mind_map_params) mind_map.update_attributes(mind_map_params)
mind_map.save mind_map.save
redirect_to "/admin/universal_table/#{mind_map.u_table.id.to_s}/mind_maps" redirect_to "/admin/universal_table/#{mind_map.u_table.id.to_s}/mind_maps"
end end
def update def update
mind_map = MindMap.find(params[:id]) mind_map = MindMap.find(params[:id])
mind_params = mind_map_params mind_params = mind_map_params
mind_params[:mind_map_data] = JSON.parse(mind_params[:mind_map_data]) mind_params[:mind_map_data] = JSON.parse(mind_params[:mind_map_data])
mind_map.update_attributes(mind_map_params) mind_map.update_attributes(mind_map_params)
mind_map.save mind_map.save
redirect_to "/admin/universal_table/#{mind_map.u_table.id.to_s}/mind_maps" redirect_to "/admin/universal_table/#{mind_map.u_table.id.to_s}/mind_maps"
end end
def destroy def destroy
uid = params[:id].split("-").last uid = params[:id].split("-").last
mind_map = MindMap.where(:uid => uid).first mind_map = MindMap.where(:uid => uid).first
table = mind_map.u_table table = mind_map.u_table
mind_map.destroy mind_map.destroy
redirect_to "/admin/universal_table/#{table.id.to_s}/mind_maps" redirect_to "/admin/universal_table/#{table.id.to_s}/mind_maps"
end end
private private
def mind_map_params def mind_map_params
params.require(:mind_map).permit! params.require(:mind_map).permit!
end end
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 render :json => {"success" => true, "title" => title}.to_json
end end
def import_data_from_excel def import_data_from_excel
workbook = RubyXL::Parser.parse(params["import_data"].tempfile) site_in_use_locales = @site_in_use_locales.sort
response = {} workbook = RubyXL::Parser.parse(params["import_data"].tempfile)
current_locale = I18n.locale response = {}
table = UTable.find(params["universal_table_id"]) rescue nil current_locale = I18n.locale
if !table.nil? table = UTable.find(params["universal_table_id"]) rescue nil
sheet = workbook[0] if table.nil?
if sheet.count <= 503 render json: { success: false, msg: "Table not found." }.to_json and return
columns = sheet[1].cells.collect.with_index{|c,i| end
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
} sheet = workbook[0]
languages = sheet[2].cells.collect{|c| if sheet.count > 503
c.value.split("-").last rescue nil render json: { success: false, msg: "More than 500 entries. Please split the entries in different files." }.to_json and return
} end
sheet.each_with_index do |row, i|
next if i < 3 # 前三列是欄位名、key、格式描述
te = TableEntry.new column_titles = sheet[0].cells.map { |c| c&.value.to_s.strip }
te.u_table = table column_keys = sheet[1].cells.map { |c| c&.value.to_s.strip }
skip = 0 column_types = sheet[2].cells.map { |c| c&.value.to_s.strip }
row.cells.each_with_index do |cell,index|
if skip > 1 # 準備欄位對應
skip = skip - 1 columns = column_keys.uniq.map.with_index do |key, i|
next tc = table.table_columns.where(key: key).first
end [i, tc]
skip = 0 end.to_h
val = cell.value rescue nil
tc = columns[index] sheet.each_with_index do |row, i|
if !tc.nil? next if i < 3 || row.cells.compact.map { |c| c.value.to_s.strip }.all?(&:blank?)
ce = ColumnEntry.new uid_val = row[0]&.value.to_s.strip rescue nil
case tc.type te = uid_val.present? ?
when "text" TableEntry.where(uid: uid_val, u_table_id: table.id).first_or_initialize :
v = {} TableEntry.new
@site_in_use_locales.sort.each_with_index do |locale,x| te.u_table = table
v[locale.to_s] = row.cells[index + x].value rescue nil skip = 0
skip = skip + 1
end tc_idx = 0
ce.text_translations = v
when "integer" row.cells.each_with_index do |cell, col_idx|
ce.number = (val.blank? ? nil : val) next if skip > 0 && (skip -= 1) >= 0
when "image"
ce.remote_image_url = val val = cell&.value
when "file" tc = columns[tc_idx]
if !val.nil? tc_idx += 1
val.split("\;").each do |remote_file| next if tc.nil?
file = ColumnEntryFile.new
file.remote_file_url = remote_file ce = te.column_entries.where(table_column_id: tc.id).first
filename = {} ce = ColumnEntry.new(table_column_id: tc.id) if ce.nil?
file.choose_lang.reject(&:empty?).each do |lang| case tc.type
filename[lang] = file.file.file.filename when "text", "editor"
end v = {}
file.file_title_translations = filename site_in_use_locales.each_with_index do |locale, offset|
# file.column_entry_id = ce.id v[locale.to_s] = row[col_idx + offset]&.value.to_s rescue ""
file.save end
ce.column_entry_files << file skip = site_in_use_locales.size - 1
end if tc.type == "text"
end ce.text_translations = v
when "editor" else
v = {} ce.content_translations = v
@site_in_use_locales.sort.each_with_index do |locale,x| end
v[locale.to_s] = row.cells[index + x].value rescue nil
skip = skip + 1 when "integer"
end ce.number = val.present? ? val.to_i : nil
ce.content_translations = v
when "date" when "image"
ce.date = val ce.remote_image_url = val if val.present?
when "period" when "file"
if tc.date_format == "yyyy" && !val.nil? file_urls = val.to_s.split(";").map(&:strip)
val = val.to_s + "/01/01" file_titles = row[col_idx + 1]&.value.to_s.split(";").map(&:strip)
end skip = 1
skip = 2
ce.period_from = val ce.column_entry_files.destroy_all
val = row.cells[index + 1].value rescue nil ce.column_entry_files = []
if tc.date_format == "yyyy" && !val.nil?
val = val.to_s + "/01/01" file_urls.each_with_index do |remote_url, file_idx|
end next if remote_url.blank?
ce.period_to = val file = ColumnEntryFile.new
end file.remote_file_url = remote_url
ce.table_column_id = tc.id
ce.save # 處理多語言標題
te.column_entries << ce titles = {}
else site_in_use_locales.each do |locale|
if index == (columns.count - 2) titles[locale.to_s] = file_titles[file_idx] rescue file.file.file.filename
create_get_table_tags(te, val.split("\;")) end
end file.file_title_translations = titles
if index == (columns.count - 1) file.save!
te.related_entries = TableEntry.where(:uid.in => val.to_s.split("\;")).pluck(:id).join(",") ce.column_entry_files << file
end end
end when "date"
end ce.date = val
te.save
te.fix_have_data when "period"
end skip = 1
response["success"] = true ce.period_from = val
response["count"] = table.table_entries.count ce.period_to = row[col_idx + 1]&.value rescue nil
response["id"] = table.id.to_s end
else
response["success"] = false ce.save!
response["msg"] = "More than 500 entries. Please split the entries in different files." te.column_entries << ce
end end
else
response["success"] = false # hashtags (倒數第2欄)
response["msg"] = "Table not found." if row.cells.count >= 2
end tags_text = row.cells[-2]&.value.to_s rescue ""
render :json => response.to_json create_get_table_tags(te, tags_text.split(";"))
end 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 def new_entry
uid = params[:universal_table_id].split("-").last uid = params[:universal_table_id].split("-").last
@ -402,4 +422,4 @@ end
def is_uuid?(str) 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) !!(str =~ /\A[\da-f]{24}\z/i || str =~ /\A[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}\z/i)
end 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
end end
def export_filtered def export_filtered
table = UTable.where(:category_id => params[:cat]).first rescue nil table = UTable.where(:category_id => params[:cat]).first rescue nil
page = Page.where(:page_id => params[:page_id]).first page = Page.where(:page_id => params[:page_id]).first
if !table.nil? return unless table
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 + '"'
end host_url = Site.first.root_url
respond_to do |format| host_url = request.protocol + request.host_with_port if host_url == "http://"
format.xlsx {
response.headers['Content-Disposition'] = excel_name
}
end
end
@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) def get_query(params)
if params["column"].present? if params["column"].present?
q = {params["column"] => params["q"]} 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 module Admin::UniversalTablesHelper
def format_date(date, format, for_editing=false) def format_date(date, format, for_editing=false)
case format case format
when "yyyy/MM/dd hh:mm" when "yyyy/MM/dd hh:mm"
f = "%Y/%m/%d %H:%M" f = "%Y/%m/%d %H:%M"
when "yyyy/MM/dd" when "yyyy/MM/dd"
f = "%Y/%m/%d" f = "%Y/%m/%d"
when "yyyy/MM" when "yyyy/MM"
f = "%Y/%m" f = "%Y/%m"
when "yyyy" when "yyyy"
f = "%Y" f = "%Y"
f = "%Y/%m" if for_editing f = "%Y/%m" if for_editing
end end
d = date.strftime(f) rescue "" d = date.strftime(f) rescue ""
return d return d
end end
def render_unique_texts(f,column,i) def render_unique_texts(f,column,i)
select_values = column.column_entries.distinct(:text) select_values = column.column_entries.distinct(:text)
select = "<select id='#{column.key}_#{i}'>" select = "<select id='#{column.key}_#{i}'>"
s = {"en" => "", "zh_tw" => ""} s = {"en" => "", "zh_tw" => ""}
select = select + "<option class='muted' value='#{s.to_json.html_safe}'>---------------Select---------------</option>" select = select + "<option class='muted' value='#{s.to_json.html_safe}'>---------------Select---------------</option>"
select_values.each do |sv| select_values.each do |sv|
select = select + "<option value='#{sv.to_json.html_safe}'>#{sv[I18n.locale]}</option>" select = select + "<option value='#{sv.to_json.html_safe}'>#{sv[I18n.locale]}</option>"
end end
select = select + "</select>" select = select + "</select>"
"<div class='select-holder'> <span>Or</span> " + select + "</div>" "<div class='select-holder'> <span>Or</span> " + select + "</div>"
end end
def render_unique_number(f,column,i) def render_unique_number(f,column,i)
select_values = column.column_entries.distinct(:number) select_values = column.column_entries.distinct(:number)
select = "<select id='#{column.key}_#{i}'>" select = "<select id='#{column.key}_#{i}'>"
s = "" s = ""
select = select + "<option class='muted' value='#{s}'>---------------Select---------------</option>" select = select + "<option class='muted' value='#{s}'>---------------Select---------------</option>"
select_values.each do |sv| select_values.each do |sv|
select = select + "<option value='#{sv}'>#{sv}</option>" select = select + "<option value='#{sv}'>#{sv}</option>"
end end
select = select + "</select>" select = select + "</select>"
"<div class='select-holder'> <span>Or</span> " + select + "</div>" "<div class='select-holder'> <span>Or</span> " + select + "</div>"
end end
end end

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

@ -1,102 +1,102 @@
class ColumnEntry class ColumnEntry
include Mongoid::Document include Mongoid::Document
include Mongoid::Timestamps include Mongoid::Timestamps
include Admin::UniversalTablesHelper include Admin::UniversalTablesHelper
include ActionView::Helpers::NumberHelper include ActionView::Helpers::NumberHelper
field :text, :localize => true field :text, :localize => true
field :content, :localize => true field :content, :localize => true
field :date, type: DateTime field :date, type: DateTime
field :period_from, type: DateTime field :period_from, type: DateTime
field :period_to, type: DateTime field :period_to, type: DateTime
field :number, type: Integer field :number, type: Integer
mount_uploader :image, ImageUploader mount_uploader :image, ImageUploader
has_many :column_entry_files, :autosave => true, :dependent => :destroy has_many :column_entry_files, :autosave => true, :dependent => :destroy
accepts_nested_attributes_for :column_entry_files, :allow_destroy => true accepts_nested_attributes_for :column_entry_files, :allow_destroy => true
after_save :save_column_entry_files after_save :save_column_entry_files
belongs_to :table_entry, index: true belongs_to :table_entry, index: true
belongs_to :table_column, index: true belongs_to :table_column, index: true
I18n.available_locales.each do |locale| I18n.available_locales.each do |locale|
index({"text.#{locale}" => 1}, { unique: false, background: true }) index({"text.#{locale}" => 1}, { unique: false, background: true })
index({"content.#{locale}" => 1}, { unique: false, background: true }) index({"content.#{locale}" => 1}, { unique: false, background: true })
end end
def type def type
self.table_column.type self.table_column.type
end end
def save_column_entry_files def save_column_entry_files
return if @skip_callback return if @skip_callback
self.column_entry_files.each do |t| self.column_entry_files.each do |t|
if t.should_destroy if t.should_destroy
t.destroy t.destroy
end end
end end
end end
def have_data(locale) def have_data(locale)
flag = nil flag = nil
case self.type case self.type
when "text" when "text"
flag = self.text_translations[locale].present? flag = self.text_translations[locale].present?
when "integer" when "integer"
flag = true flag = true
when "editor" when "editor"
flag = self.content_translations[locale].present? flag = self.content_translations[locale].present?
when "date" when "date"
flag = self.date.present? flag = self.date.present?
when "period" when "period"
flag = self.period_from.present? || self.period_to.present? flag = self.period_from.present? || self.period_to.present?
when "image" when "image"
flag = self.image.present? flag = self.image.present?
when "file" when "file"
flag = false flag = false
self.column_entry_files.each do |entry_file| self.column_entry_files.each do |entry_file|
next unless entry_file.choose_lang_display(locale) && entry_file.file.present? next unless entry_file.choose_lang_display(locale) && entry_file.file.present?
flag = true flag = true
end end
else else
flag = true flag = true
end end
flag flag
end end
def get_frontend_text(column) def get_frontend_text(column)
text = "" text = ""
case self.type case self.type
when "text" when "text"
text = self.text text = self.text
when "integer" when "integer"
text = self.number text = self.number
when "editor" when "editor"
text = self.content text = self.content
when "date" when "date"
text = format_date(self.date, column.date_format) text = format_date(self.date, column.date_format)
when "period" when "period"
text = format_date(self.period_from, column.date_format) + " ~ " + format_date(self.period_to, column.date_format) text = format_date(self.period_from, column.date_format) + " ~ " + format_date(self.period_to, column.date_format)
text = "" if text.starts_with?(" ~") text = "" if text.starts_with?(" ~")
when "image" when "image"
text = "<img src='#{self.image.thumb.url}' class='image-preview' />" text = "<img src='#{self.image.thumb.url}' class='image-preview' />"
when "file" when "file"
locale = I18n.locale.to_s locale = I18n.locale.to_s
text = "<ul class=\"column_entry_files\">" text = "<ul class=\"column_entry_files\">"
self.column_entry_files.desc(:sort_number).each do |entry_file| self.column_entry_files.desc(:sort_number).each do |entry_file|
next unless entry_file.choose_lang_display(locale) next unless entry_file.choose_lang_display(locale)
file_title = entry_file.get_file_title file_title = entry_file.get_file_title
if entry_file.file.content_type.start_with?('audio/') 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>" 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 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>" 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
end end
text += "</ul>" text += "</ul>"
end end
text text
end end
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 mount_uploader :file, AssetUploader
field :file_title, localize: true field :file_title, type: String, localize: true
# field :description # field :description
field :download_count, type: Integer, default: 0 field :download_count, type: Integer, default: 0
field :choose_lang, :type => Array, :default => I18n.available_locales.map{|l| l.to_s} 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 class MindMap
include Mongoid::Document include Mongoid::Document
include Mongoid::Timestamps include Mongoid::Timestamps
include Slug include Slug
field :title, as: :slug_title, localize: true field :title, as: :slug_title, localize: true
field :mind_map_data, type: Array, default: [] field :mind_map_data, type: Array, default: []
belongs_to :u_table belongs_to :u_table
# has_many :mind_map_nodes, :dependent => :destroy # has_many :mind_map_nodes, :dependent => :destroy
end end

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

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

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

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

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

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

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

@ -1,42 +1,42 @@
class UTable class UTable
include Mongoid::Document include Mongoid::Document
include Mongoid::Timestamps include Mongoid::Timestamps
include OrbitCategory::Categorizable include OrbitCategory::Categorizable
include Slug include Slug
field :title, as: :slug_title, localize: true field :title, as: :slug_title, localize: true
field :ordered_with_sort_number, type: Boolean, default: false field :ordered_with_sort_number, type: Boolean, default: false
field :sort_number_order_direction, type: String, default: 'desc' field :sort_number_order_direction, type: String, default: 'desc'
field :ordered_with_created_at, type: Boolean, default: true field :ordered_with_created_at, type: Boolean, default: true
field :created_at_order_direction, type: String, default: 'desc' field :created_at_order_direction, type: String, default: 'desc'
has_many :table_columns, :dependent => :destroy has_many :table_columns, :dependent => :destroy
has_many :table_entries, :dependent => :destroy has_many :table_entries, :dependent => :destroy
has_many :mind_maps, :dependent => :destroy has_many :mind_maps, :dependent => :destroy
accepts_nested_attributes_for :table_columns, :allow_destroy => true accepts_nested_attributes_for :table_columns, :allow_destroy => true
FIELD_TYPES = ["text", "integer", "editor", "image", "date", "period", "file"] FIELD_TYPES = ["text", "integer", "editor", "image", "date", "period", "file"]
DATE_FORMATS = ["yyyy/MM/dd hh:mm", "yyyy/MM/dd","yyyy/MM", "yyyy"] DATE_FORMATS = ["yyyy/MM/dd hh:mm", "yyyy/MM/dd","yyyy/MM", "yyyy"]
AUDIO_EXTENSIONS = %w[.mp3 .wav .ogg .m4a .aac .flac] AUDIO_EXTENSIONS = %w[.mp3 .wav .ogg .m4a .aac .flac]
def default_ordered def default_ordered
if self.ordered_with_created_at if self.ordered_with_created_at
sort_column = 'created_at' sort_column = 'created_at'
direction = self.created_at_order_direction direction = self.created_at_order_direction
elsif self.ordered_with_sort_number elsif self.ordered_with_sort_number
sort_column = 'sort_number' sort_column = 'sort_number'
direction = self.sort_number_order_direction direction = self.sort_number_order_direction
else else
sort_column = self.table_columns.where(default_ordered_field: true).first sort_column = self.table_columns.where(default_ordered_field: true).first
if sort_column if sort_column
direction = sort_column.order_direction direction = sort_column.order_direction
else else
sort_column = 'created_at' sort_column = 'created_at'
direction = self.created_at_order_direction direction = self.created_at_order_direction
end end
end end
[sort_column,direction] [sort_column,direction]
end end
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"> <fieldset class="utable-heading-wrap">
<div class="utable-heading-header"> <div class="utable-heading-header">
<h4><%= t("universal_table.table_name") %> - <%= @table.title %></h4> <h4><%= t("universal_table.table_name") %> - <%= @table.title %></h4>
</div> </div>
<div class="control-group"> <div class="control-group">
<div class="controls"> <div class="controls">
<div class="input-append"> <div class="input-append">
<div class="tab-content"> <div class="tab-content">
<% @site_in_use_locales.each do |locale| %> <% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %> <% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<div class="tab-pane fade <%= active %>" id="mind_map_<%= locale.to_s %>"> <div class="tab-pane fade <%= active %>" id="mind_map_<%= locale.to_s %>">
<%= f.fields_for :title_translations do |f| %> <%= f.fields_for :title_translations do |f| %>
<%= f.text_field locale, :placeholder => "Title", :value => @mind_map.title_translations[locale] %> <%= f.text_field locale, :placeholder => "Title", :value => @mind_map.title_translations[locale] %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
<div class="btn-group" data-toggle="buttons-radio"> <div class="btn-group" data-toggle="buttons-radio">
<% @site_in_use_locales.each do |locale| %> <% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active" : "") %> <% 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"}%> <%= link_to t(locale).to_s,"#mind_map_#{locale.to_s}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%>
<% end %> <% end %>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset class="utable-heading-wrap"> <fieldset class="utable-heading-wrap">
<div class="utable-heading-header"> <div class="utable-heading-header">
<h4><%= t("universal_table.mind_map") %></h4> <h4><%= t("universal_table.mind_map") %></h4>
</div> </div>
<div class="control-group"> <div class="control-group">
<div class="controls"> <div class="controls">
<button id="toggle_editable"><%= t("universal_table.disable_editing") %></button> <button id="toggle_editable"><%= t("universal_table.disable_editing") %></button>
</div> </div>
<div id="jsmind_container"></div> <div id="jsmind_container"></div>
</div> </div>
</fieldset> </fieldset>
<fieldset class="utable-content"> <fieldset class="utable-content">
<div class="form-actions"> <div class="form-actions">
<%= f.hidden_field :mind_map_data, id: "mind_map_data_field", value: "[]" %> <%= f.hidden_field :mind_map_data, id: "mind_map_data_field", value: "[]" %>
<%= f.hidden_field :u_table_id, value: @table.id %> <%= f.hidden_field :u_table_id, value: @table.id %>
<input class="btn btn-primary pull-right" name="commit" type="submit" value="<%= t("save") %>"> <input class="btn btn-primary pull-right" name="commit" type="submit" value="<%= t("save") %>">
</div> </div>
</fieldset> </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"> <table class="table main-list">
<thead> <thead>
<tr class="sort-header"> <tr class="sort-header">
<% @table_fields.each do |f| %> <% @table_fields.each do |f| %>
<%= thead(f) %> <%= thead(f) %>
<% end %> <% end %>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% @mind_maps.each do |mindmap| %> <% @mind_maps.each do |mindmap| %>
<tr id="mindmap_<%= mindmap.id.to_s %>"> <tr id="mindmap_<%= mindmap.id.to_s %>">
<td> <td>
<a href="<%= admin_mind_map_path(mindmap) %>"><%= mindmap.title %></a> <a href="<%= admin_mind_map_path(mindmap) %>"><%= mindmap.title %></a>
<div class="quick-edit"> <div class="quick-edit">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li><a href="<%= edit_admin_mind_map_path(mindmap) %>"><%= t(:edit) %></a></li> <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> <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> </ul>
</div> </div>
</td> </td>
<td> <td>
<%= mindmap.created_at.strftime("%Y-%m-%d") %> <%= mindmap.created_at.strftime("%Y-%m-%d") %>
</td> </td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<%= <%=
content_tag :div, class: "bottomnav clearfix" do content_tag :div, class: "bottomnav clearfix" do
content_tag(:div, paginate(@mind_maps), class: "pagination pagination-centered") + 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") 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 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 %> <% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %> <%= stylesheet_link_tag "universal_table/universal-table" %>
<%= stylesheet_link_tag "mind_map/mindmap" %> <%= stylesheet_link_tag "mind_map/mindmap" %>
<% end %> <% 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| %> <%= 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} %> <%= render :partial => "form", locals: {f: f} %>
<% end %> <% end %>
<script type="module"> <script type="module">
import '/assets/mind_map/utils/custom.overrides.js' import '/assets/mind_map/utils/custom.overrides.js'
import '/assets/mind_map/jsmind/plugins/jsmind.draggable-node.js' import '/assets/mind_map/jsmind/plugins/jsmind.draggable-node.js'
import { initJsmind, getJsmindData } from '/assets/mind_map/utils/custom.main.js' import { initJsmind, getJsmindData } from '/assets/mind_map/utils/custom.main.js'
import { INITIAL_MIND } from '/assets/mind_map/utils/custom.config.js' import { INITIAL_MIND } from '/assets/mind_map/utils/custom.config.js'
// 操控心智圖是否可編輯 // 操控心智圖是否可編輯
// Control whether the mind map is editable // Control whether the mind map is editable
let isEditable = true let isEditable = true
// 心智圖實例 // 心智圖實例
// Mind map instance // Mind map instance
let jm let jm
// 心智圖初始數據 // 心智圖初始數據
// Initial mind map data // Initial mind map data
let mind = { let mind = {
meta: {}, meta: {},
format: 'node_array', format: 'node_array',
data: <%= raw @mind_map.mind_map_data.to_json %> data: <%= raw @mind_map.mind_map_data.to_json %>
} }
// 心智圖自訂選項(可參考 jsmind 官方文檔) // 心智圖自訂選項(可參考 jsmind 官方文檔)
// Custom options for the mind map (refer to the jsmind official documentation) // Custom options for the mind map (refer to the jsmind official documentation)
const options = { const options = {
container: 'jsmind_container', container: 'jsmind_container',
editable: isEditable, editable: isEditable,
theme: 'primary', theme: 'primary',
mode: 'full', mode: 'full',
tableUID: '<%= @table.uid %>', tableUID: '<%= @table.uid %>',
text: { text: {
addNode: "<%= t("universal_table.add_node") %>", addNode: "<%= t("universal_table.add_node") %>",
deleteNode: "<%= t("universal_table.delete_node") %>", deleteNode: "<%= t("universal_table.delete_node") %>",
strokeColor: "<%= t("universal_table.stroke_color") %>", strokeColor: "<%= t("universal_table.stroke_color") %>",
bgColor: "<%= t("universal_table.bg_color") %>", bgColor: "<%= t("universal_table.bg_color") %>",
textColor: "<%= t("universal_table.text_color") %>" textColor: "<%= t("universal_table.text_color") %>"
}, },
view: { view: {
engine: 'svg', engine: 'svg',
draggable: true, draggable: true,
node_overflow: 'wrap', node_overflow: 'wrap',
}, },
shortcut: { shortcut: {
mapping: { mapping: {
// 避免與 Toolbar 按下 Enter 事件衝突 // 避免與 Toolbar 按下 Enter 事件衝突
// Avoid conflicts with the Enter key event in the Toolbar // Avoid conflicts with the Enter key event in the Toolbar
addbrother: 2048 + 13, addbrother: 2048 + 13,
}, },
}, },
} }
// 初始化心智圖並掛載實例 // 初始化心智圖並掛載實例
// Initialize the mind map and attach the instance // Initialize the mind map and attach the instance
jm = initJsmind(mind, options, isEditable) jm = initJsmind(mind, options, isEditable)
// 儲存當前數據 // 儲存當前數據
// Save the current data // Save the current data
// document.getElementById('save_mind_map').addEventListener('click', (e) => { // document.getElementById('save_mind_map').addEventListener('click', (e) => {
// e.preventDefault(); // e.preventDefault();
// e.stopPropagation(); // e.stopPropagation();
// let data = getJsmindData(jm); // let data = getJsmindData(jm);
// console.log(data); // console.log(data);
// }) // })
// 調整可編輯狀態 // 調整可編輯狀態
// Toggle the editable state // Toggle the editable state
document.getElementById('toggle_editable').addEventListener('click', (e) => { document.getElementById('toggle_editable').addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
isEditable = !isEditable; isEditable = !isEditable;
e.target.innerHTML = isEditable ? '<%= t("universal_table.disable_editing") %>' : '<%= t("universal_table.enable_editing") %>'; e.target.innerHTML = isEditable ? '<%= t("universal_table.disable_editing") %>' : '<%= t("universal_table.enable_editing") %>';
mind = getJsmindData(jm); mind = getJsmindData(jm);
jm = initJsmind(mind, options, isEditable); jm = initJsmind(mind, options, isEditable);
return false; return false;
}) })
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const form = document.getElementById("mind_map_form"); const form = document.getElementById("mind_map_form");
const hiddenField = document.getElementById("mind_map_data_field"); const hiddenField = document.getElementById("mind_map_data_field");
form.addEventListener("submit", function (e) { form.addEventListener("submit", function (e) {
const mindMapData = getJsmindData(jm); const mindMapData = getJsmindData(jm);
console.log(mindMapData); console.log(mindMapData);
hiddenField.value = JSON.stringify(mindMapData.data); hiddenField.value = JSON.stringify(mindMapData.data);
}); });
}); });
</script> </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 %> <% content_for :page_specific_javascript do %>
<%= javascript_include_tag "lib/jquery.form" %> <%= javascript_include_tag "lib/jquery.form" %>
<% end %> <% end %>
<div id="index_table"> <div id="index_table">
<%= render 'index'%> <%= render 'index'%>
</div> </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 %> <% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %> <%= stylesheet_link_tag "universal_table/universal-table" %>
<%= stylesheet_link_tag "mind_map/mindmap" %> <%= stylesheet_link_tag "mind_map/mindmap" %>
<% end %> <% end %>
<%= form_for @mind_map, url: admin_mind_maps_path, html: {class: "form-horizontal main-forms", id: "mind_map_form"} do |f| %> <%= 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} %> <%= render :partial => "form", locals: {f: f} %>
<% end %> <% end %>
<script type="module"> <script type="module">
import '/assets/mind_map/utils/custom.overrides.js' import '/assets/mind_map/utils/custom.overrides.js'
import '/assets/mind_map/jsmind/plugins/jsmind.draggable-node.js' import '/assets/mind_map/jsmind/plugins/jsmind.draggable-node.js'
import { initJsmind, getJsmindData } from '/assets/mind_map/utils/custom.main.js' import { initJsmind, getJsmindData } from '/assets/mind_map/utils/custom.main.js'
import { INITIAL_MIND } from '/assets/mind_map/utils/custom.config.js' import { INITIAL_MIND } from '/assets/mind_map/utils/custom.config.js'
// 操控心智圖是否可編輯 // 操控心智圖是否可編輯
// Control whether the mind map is editable // Control whether the mind map is editable
let isEditable = true let isEditable = true
// 心智圖實例 // 心智圖實例
// Mind map instance // Mind map instance
let jm let jm
// 心智圖初始數據 // 心智圖初始數據
// Initial mind map data // Initial mind map data
let mind = INITIAL_MIND let mind = INITIAL_MIND
// 心智圖自訂選項(可參考 jsmind 官方文檔) // 心智圖自訂選項(可參考 jsmind 官方文檔)
// Custom options for the mind map (refer to the jsmind official documentation) // Custom options for the mind map (refer to the jsmind official documentation)
const options = { const options = {
container: 'jsmind_container', container: 'jsmind_container',
editable: isEditable, editable: isEditable,
theme: 'primary', theme: 'primary',
mode: 'full', mode: 'full',
tableUID: '<%= @table.uid %>', tableUID: '<%= @table.uid %>',
text: { text: {
addNode: "<%= t("universal_table.add_node") %>", addNode: "<%= t("universal_table.add_node") %>",
deleteNode: "<%= t("universal_table.delete_node") %>", deleteNode: "<%= t("universal_table.delete_node") %>",
strokeColor: "<%= t("universal_table.stroke_color") %>", strokeColor: "<%= t("universal_table.stroke_color") %>",
bgColor: "<%= t("universal_table.bg_color") %>", bgColor: "<%= t("universal_table.bg_color") %>",
textColor: "<%= t("universal_table.text_color") %>" textColor: "<%= t("universal_table.text_color") %>"
}, },
view: { view: {
engine: 'svg', engine: 'svg',
draggable: true, draggable: true,
node_overflow: 'wrap', node_overflow: 'wrap',
}, },
shortcut: { shortcut: {
mapping: { mapping: {
// 避免與 Toolbar 按下 Enter 事件衝突 // 避免與 Toolbar 按下 Enter 事件衝突
// Avoid conflicts with the Enter key event in the Toolbar // Avoid conflicts with the Enter key event in the Toolbar
addbrother: 2048 + 13, addbrother: 2048 + 13,
}, },
}, },
} }
// 初始化心智圖並掛載實例 // 初始化心智圖並掛載實例
// Initialize the mind map and attach the instance // Initialize the mind map and attach the instance
jm = initJsmind(mind, options, isEditable) jm = initJsmind(mind, options, isEditable)
// 儲存當前數據 // 儲存當前數據
// Save the current data // Save the current data
// document.getElementById('save_mind_map').addEventListener('click', (e) => { // document.getElementById('save_mind_map').addEventListener('click', (e) => {
// e.preventDefault(); // e.preventDefault();
// e.stopPropagation(); // e.stopPropagation();
// let data = getJsmindData(jm); // let data = getJsmindData(jm);
// console.log(data); // console.log(data);
// }) // })
// 調整可編輯狀態 // 調整可編輯狀態
// Toggle the editable state // Toggle the editable state
document.getElementById('toggle_editable').addEventListener('click', (e) => { document.getElementById('toggle_editable').addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
isEditable = !isEditable; isEditable = !isEditable;
e.target.innerHTML = isEditable ? '<%= t("universal_table.disable_editing") %>' : '<%= t("universal_table.enable_editing") %>'; e.target.innerHTML = isEditable ? '<%= t("universal_table.disable_editing") %>' : '<%= t("universal_table.enable_editing") %>';
mind = getJsmindData(jm); mind = getJsmindData(jm);
jm = initJsmind(mind, options, isEditable); jm = initJsmind(mind, options, isEditable);
return false; return false;
}) })
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const form = document.getElementById("mind_map_form"); const form = document.getElementById("mind_map_form");
const hiddenField = document.getElementById("mind_map_data_field"); const hiddenField = document.getElementById("mind_map_data_field");
form.addEventListener("submit", function (e) { form.addEventListener("submit", function (e) {
const mindMapData = getJsmindData(jm); const mindMapData = getJsmindData(jm);
console.log(mindMapData); console.log(mindMapData);
hiddenField.value = JSON.stringify(mindMapData.data); hiddenField.value = JSON.stringify(mindMapData.data);
}); });
}); });
</script> </script>

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

@ -1,102 +1,102 @@
<% if !defined?(i) %> <% if !defined?(i) %>
<div class="attributes"> <div class="attributes">
<% end %> <% end %>
<div class="attributes-header clearfix"> <div class="attributes-header clearfix">
<a class="btn btn-mini pull-right btn-danger delete" href="#"><i class="icon-trash"></i> Delete</a> <a class="btn btn-mini pull-right btn-danger delete" href="#"><i class="icon-trash"></i> Delete</a>
<% if defined?(i) %> <% if defined?(i) %>
<%= f.hidden_field :_destroy, :value => "false", :class => "attribute_field_to_delete" %> <%= f.hidden_field :_destroy, :value => "false", :class => "attribute_field_to_delete" %>
<h4 class="draggable"><i class="icons-list-2"></i> <%= column.title %></h4> <h4 class="draggable"><i class="icons-list-2"></i> <%= column.title %></h4>
<% else %> <% else %>
<h4 class="draggable"><i class="icons-list-2"></i> ColumnXX</h4> <h4 class="draggable"><i class="icons-list-2"></i> ColumnXX</h4>
<% end %> <% end %>
</div> </div>
<div class="attributes-body"> <div class="attributes-body">
<div class="control-group"> <div class="control-group">
<label class="control-label muted" for="key_0">Key</label> <label class="control-label muted" for="key_0">Key</label>
<div class="controls"> <div class="controls">
<%= f.text_field :key, :autocomplete => "off", :'data-type' => 'key' %> <%= f.text_field :key, :autocomplete => "off", :'data-type' => 'key' %>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label muted" for="">Title</label> <label class="control-label muted" for="">Title</label>
<div class="controls"> <div class="controls">
<div class="input-append"> <div class="input-append">
<div class="tab-content"> <div class="tab-content">
<% @site_in_use_locales.each do |locale| %> <% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %> <% 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}") %> <% 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 %>"> <div class="tab-pane fade in <%= active %>" id="<%= id %>">
<%= f.fields_for :title_translations do |f| %> <%= f.fields_for :title_translations do |f| %>
<%= f.text_field locale, :value => column.title_translations[locale] %> <%= f.text_field locale, :value => column.title_translations[locale] %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
<div class="btn-group" data-toggle="buttons-radio"> <div class="btn-group" data-toggle="buttons-radio">
<% @site_in_use_locales.each do |locale| %> <% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active" : "") %> <% 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}") %> <% 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"}%> <%= link_to t(locale).to_s,"##{id}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%>
<% end %> <% end %>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label muted" for="">Display in index</label> <label class="control-label muted" for="">Display in index</label>
<div class="controls"> <div class="controls">
<label class="radio inline"> <label class="radio inline">
<%= f.radio_button :display_in_index, "true" %>Yes <%= f.radio_button :display_in_index, "true" %>Yes
</label> </label>
<label class="radio inline"> <label class="radio inline">
<%= f.radio_button :display_in_index, "false" %>No <%= f.radio_button :display_in_index, "false" %>No
</label> </label>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label muted" for=""><%= t('universal_table.default_ordered_field') %></label> <label class="control-label muted" for=""><%= t('universal_table.default_ordered_field') %></label>
<div class="controls"> <div class="controls">
<div> <div>
<%= f.check_box :default_ordered_field, class: 'default_ordered_field' %> <%= f.check_box :default_ordered_field, class: 'default_ordered_field' %>
</div> </div>
<div class="order_direction<%= ' hidden' if !f.object.default_ordered_field %>"> <div class="order_direction<%= ' hidden' if !f.object.default_ordered_field %>">
<%= f.select :order_direction,['desc','asc'].map{|v| [t("universal_table.#{v}"),v]} %> <%= f.select :order_direction,['desc','asc'].map{|v| [t("universal_table.#{v}"),v]} %>
</div> </div>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label muted" for="">Type</label> <label class="control-label muted" for="">Type</label>
<div class="controls"> <div class="controls">
<% select_values = UTable::FIELD_TYPES.collect{|ft| [ft.capitalize,ft]} %> <% select_values = UTable::FIELD_TYPES.collect{|ft| [ft.capitalize,ft]} %>
<%= f.select :type, select_values, {}, {class: "type-selector"} %> <%= f.select :type, select_values, {}, {class: "type-selector"} %>
<span class="link_to_show <%= (!defined?(i) || column.type == "text" || column.type == "integer") ? "" : "hide" %>"> <span class="link_to_show <%= (!defined?(i) || column.type == "text" || column.type == "integer") ? "" : "hide" %>">
<label class="checkbox inline attributes-checkbox "> <label class="checkbox inline attributes-checkbox ">
<%= f.check_box :is_link_to_show %> Link to show <%= f.check_box :is_link_to_show %> Link to show
</label> </label>
<label class="checkbox inline attributes-checkbox"> <label class="checkbox inline attributes-checkbox">
<%= f.check_box :make_categorizable %> Categorizable <%= f.check_box :make_categorizable %> Categorizable
</label> </label>
<label class="checkbox inline attributes-checkbox "> <label class="checkbox inline attributes-checkbox ">
<%= f.check_box :is_searchable %> Searchable <%= f.check_box :is_searchable %> Searchable
</label> </label>
</span> </span>
<% select_values = UTable::DATE_FORMATS.collect{|ft| [ft.upcase,ft]} %> <% 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" %>"> <label class="checkbox date_format inline attributes-checkbox <%= column.type == "date" || column.type == "period" ? "" : "hide" %>">
Date Format <%= f.select :date_format, select_values%> Date Format <%= f.select :date_format, select_values%>
</label> </label>
</div> </div>
</div> </div>
<% if defined?(i) %> <% if defined?(i) %>
<% if column.order.nil? %> <% if column.order.nil? %>
<%= f.hidden_field :order, :value => i, :class => "order-hidden-field" %> <%= f.hidden_field :order, :value => i, :class => "order-hidden-field" %>
<% else %> <% else %>
<%= f.hidden_field :order, :class => "order-hidden-field" %> <%= f.hidden_field :order, :class => "order-hidden-field" %>
<% end %> <% end %>
<% else %> <% else %>
<%= f.hidden_field :order, :value=> "XXX", :class => "order-hidden-field" %> <%= f.hidden_field :order, :value=> "XXX", :class => "order-hidden-field" %>
<% end %> <% end %>
</div> </div>
<% if !defined?(i) %> <% if !defined?(i) %>
</div> </div>
<% end %> <% 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"> <div class="control-group">
<%= f.label :date, column.title, :class => "control-label" %> <%= f.label :date, column.title, :class => "control-label" %>
<div class="controls"> <div class="controls">
<div> <div>
<div class="default_picker input-append" style=""> <div class="default_picker input-append" style="">
<% v = !date_field.new_record? ? format_date(date_field.date, column.date_format, true) : "" %> <% 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)} %> <%= 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 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> <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> </div>
<% if !date_field.new_record? %> <% if !date_field.new_record? %>
<%= f.hidden_field :id %> <%= f.hidden_field :id %>
<% else %> <% else %>
<%= f.hidden_field :table_column_id, :value => column.id %> <%= f.hidden_field :table_column_id, :value => column.id %>
<% end %> <% end %>
</div> </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"> <div id="data-table" class="ut-table">
<table class="table main-list"> <table class="table main-list">
<thead> <thead>
<tr class="sort-header"> <tr class="sort-header">
<% @table_fields.each do |field| %> <% @table_fields.each do |field| %>
<% <%
field_text = field.to_s.include?('.') ? t(field.to_s) : field.to_s field_text = field.to_s.include?('.') ? t(field.to_s) : field.to_s
sort = field.to_s.split('.')[-1] sort = field.to_s.split('.')[-1]
active = params[:sort].eql? sort active = params[:sort].eql? sort
order = active ? (["asc", "desc"]-[params[:order]]).first : "asc" 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>" arrow = (order.eql? "desc") ? "<b class='icons-arrow-up-3'></b>" : "<b class='icons-arrow-down-4'></b>"
klass = field.eql?(:title) ? "span5" : "span2" klass = field.eql?(:title) ? "span5" : "span2"
th_data = "<a href='?sort=#{sort}&order=#{order}'>#{field_text} #{active ? arrow : ""}</a>" th_data = "<a href='?sort=#{sort}&order=#{order}'>#{field_text} #{active ? arrow : ""}</a>"
%> %>
<th class='<%= klass %> <%= active ? "active" : "" %>'><%= th_data.html_safe %></th> <th class='<%= klass %> <%= active ? "active" : "" %>'><%= th_data.html_safe %></th>
<% end %> <% end %>
</tr> </tr>
</thead> </thead>
<tbody id="sortable"> <tbody id="sortable">
<% can_edit = can_edit_or_delete?(@entries.first.u_table) if !(@entries.first.nil?) %> <% can_edit = can_edit_or_delete?(@entries.first.u_table) if !(@entries.first.nil?) %>
<% @entries.each do |entry| %> <% @entries.each do |entry| %>
<tr data-id="<%= entry.id %>"> <tr data-id="<%= entry.id %>">
<td> <td>
<%= number_field_tag nil,entry.sort_number,class: 'sort_number',step: 1 %> <%= number_field_tag nil,entry.sort_number,class: 'sort_number',step: 1 %>
</td> </td>
<% @columns.each_with_index do |column, index| %> <% @columns.each_with_index do |column, index| %>
<% ce = entry.column_entries.where(:table_column_id => column.id).first rescue nil %> <% ce = entry.column_entries.where(:table_column_id => column.id).first rescue nil %>
<% if !ce.nil? %> <% if !ce.nil? %>
<td> <td>
<% case ce.type %> <% case ce.type %>
<% when "text" %> <% when "text" %>
<%= ce.text %> <%= ce.text %>
<% when "integer" %> <% when "integer" %>
<%= ce.number %> <%= ce.number %>
<% when "editor" %> <% when "editor" %>
<%= ce.content.html_safe rescue "" %> <%= ce.content.html_safe rescue "" %>
<% when "image" %> <% when "image" %>
<div class="image-expander"> <div class="image-expander">
<% if !ce.image.nil? %> <% if !ce.image.nil? %>
<a href="<%= ce.image.url %>" target="_blank"><img src="<%= ce.image.thumb.url %>" class="image-preview" /></a> <a href="<%= ce.image.url %>" target="_blank"><img src="<%= ce.image.thumb.url %>" class="image-preview" /></a>
<% end %> <% end %>
</div> </div>
<% when "date" %> <% when "date" %>
<%= format_date(ce.date, column.date_format) %> <%= format_date(ce.date, column.date_format) %>
<% when "period" %> <% when "period" %>
<% if !ce.period_from.nil? %> <% if !ce.period_from.nil? %>
<%= format_date(ce.period_from, column.date_format) %> ~ <%= format_date(ce.period_to, column.date_format) %> <%= format_date(ce.period_from, column.date_format) %> ~ <%= format_date(ce.period_to, column.date_format) %>
<% end %> <% end %>
<% end %> <% end %>
<% if index == 0 && can_edit %> <% if index == 0 && can_edit %>
<div class="quick-edit"> <div class="quick-edit">
<ul class="nav nav-pills"> <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_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> <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> </ul>
</div> </div>
<% end %> <% end %>
</td> </td>
<% else %> <% else %>
<td> <td>
&nbsp; &nbsp;
</td> </td>
<% end %> <% end %>
<% end %> <% end %>
<td> <td>
<%= entry.created_at %> <%= entry.created_at %>
</td> </td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
</div> </div>

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

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

View File

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

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

@ -1,289 +1,289 @@
<% # encoding: utf-8 %> <% # encoding: utf-8 %>
<% content_for :page_specific_css do %> <% content_for :page_specific_css do %>
<style type="text/css"> <style type="text/css">
.sort-order-icon{ .sort-order-icon{
font-size: 25px; font-size: 25px;
cursor: move; cursor: move;
} }
</style> </style>
<% end %> <% end %>
<% content_for :page_specific_javascript do %> <% content_for :page_specific_javascript do %>
<%= javascript_include_tag "lib/file-type" %> <%= javascript_include_tag "lib/file-type" %>
<%= javascript_include_tag "lib/module-area" %> <%= javascript_include_tag "lib/module-area" %>
<%= javascript_include_tag "lib/jquery-ui-sortable.min" %> <%= javascript_include_tag "lib/jquery-ui-sortable.min" %>
<% end %> <% end %>
<style> <style>
#fileupload { #fileupload {
position: relative; position: relative;
clear: both; clear: both;
overflow: hidden; overflow: hidden;
height: 254px; height: 254px;
} }
#fileupload #dropzone.drop { #fileupload #dropzone.drop {
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
border: 2px dashed #0088CC; border: 2px dashed #0088CC;
border-radius: 10px; border-radius: 10px;
color: #0088CC; color: #0088CC;
background-color: #FFFFFF; background-color: #FFFFFF;
z-index: 0; z-index: 0;
} }
#fileupload #dropzone { #fileupload #dropzone {
padding: 30px; padding: 30px;
text-align: center; text-align: center;
font-size: 3em; font-size: 3em;
font-family: 'Raleway'; font-family: 'Raleway';
line-height: 1.2em; line-height: 1.2em;
color: #e4e4e4; color: #e4e4e4;
} }
#fileupload #dropzone.in { #fileupload #dropzone.in {
opacity: .7; opacity: .7;
z-index: 2; z-index: 2;
border-color: #faa732; border-color: #faa732;
color: #faa732; color: #faa732;
} }
</style> </style>
<div class="control-group"> <div class="control-group">
<%= f.label :text, column.title, :class => "control-label" %> <%= f.label :text, column.title, :class => "control-label" %>
<div class="controls"> <div class="controls">
<p class="add-btn"> <p class="add-btn">
<%= hidden_field_tag 'column_entry_file_field_count', file_field.column_entry_files.count %> <%= 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> <a id="add_file" class="trigger btn btn-small btn-primary"><i class="icons-plus"></i> <%= t(:add) %></a>
</p> </p>
<hr> <hr>
<!-- Add --> <!-- Add -->
<div class="add-target" id="add-target"></div> <div class="add-target" id="add-target"></div>
<!-- Exist --> <!-- Exist -->
<% if file_field && !file_field.column_entry_files.blank? %> <% if file_field && !file_field.column_entry_files.blank? %>
<div class="exist plugin-sortable"> <div class="exist plugin-sortable">
<% file_field.column_entry_files.desc(:sort_number).each_with_index do |column_entry_file, i| %> <% 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| %> <%= 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} %> <%= render :partial => 'form_file', :locals => {:f => f, :i => i,:form_file => column_entry_file} %>
<% end %> <% end %>
<% end %> <% end %>
<hr> <hr>
</div> </div>
<% end %> <% end %>
<div id="fileupload" ondrop="dropHandler(event);" ondragover="dragOverHandler(event);" ondragleave="(function(){$('#dropzone').removeClass('in');})()"> <div id="fileupload" ondrop="dropHandler(event);" ondragover="dragOverHandler(event);" ondragleave="(function(){$('#dropzone').removeClass('in');})()">
<div id="dropzone" class="drop"> <div id="dropzone" class="drop">
<div data-icons=""></div> <div data-icons=""></div>
<%=t("universal_table.drag_file_to_here")%> <%=t("universal_table.drag_file_to_here")%>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<% if !file_field.new_record? %> <% if !file_field.new_record? %>
<%= f.hidden_field :id %> <%= f.hidden_field :id %>
<% else %> <% else %>
<%= f.hidden_field :table_column_id, :value => column.id %> <%= f.hidden_field :table_column_id, :value => column.id %>
<% end %> <% end %>
</div> </div>
<% content_for :page_specific_javascript do %> <% content_for :page_specific_javascript do %>
<script> <script>
if (!FileReader.prototype.readAsBinaryString) { if (!FileReader.prototype.readAsBinaryString) {
console.log('readAsBinaryString definition not found'); console.log('readAsBinaryString definition not found');
FileReader.prototype.readAsBinaryString = function (fileData) { FileReader.prototype.readAsBinaryString = function (fileData) {
var binary = ''; var binary = '';
var pk = this; var pk = this;
var reader = new FileReader(); var reader = new FileReader();
reader.onload = function (e) { reader.onload = function (e) {
var bytes = new Uint8Array(reader.result); var bytes = new Uint8Array(reader.result);
var length = bytes.byteLength; var length = bytes.byteLength;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
var a = bytes[i]; var a = bytes[i];
var b = String.fromCharCode(a) var b = String.fromCharCode(a)
binary += b; binary += b;
} }
pk.content = binary; pk.content = binary;
$(pk).trigger('onload'); $(pk).trigger('onload');
} }
reader.readAsArrayBuffer(fileData); reader.readAsArrayBuffer(fileData);
} }
} }
function FileListItems (files) { function FileListItems (files) {
var b; var b;
try{ try{
b = new DataTransfer(); b = new DataTransfer();
}catch(e){ }catch(e){
if(window.dataTransfer){ if(window.dataTransfer){
b = window.dataTransfer; b = window.dataTransfer;
}else{ }else{
if(typeof(ClipboardEvent) == "undefined"){ //IE if(typeof(ClipboardEvent) == "undefined"){ //IE
b = new DataTransfer(); b = new DataTransfer();
}else{ }else{
b = new ClipboardEvent("").clipboardData; b = new ClipboardEvent("").clipboardData;
} }
} }
} }
if(b.items){ if(b.items){
b.items.clear(); b.items.clear();
}else{ }else{
if(b.files.length != 0 ){ if(b.files.length != 0 ){
var files_length = b.files.length; var files_length = b.files.length;
for(var i = files_length - 1;i >= 0;i = i - 1){ for(var i = files_length - 1;i >= 0;i = i - 1){
delete b.files[i]; delete b.files[i];
} }
} }
if(b.files.length != 0){ if(b.files.length != 0){
return files return files
} }
} }
for (var i = 0, len = files.length; i<len; i++){ for (var i = 0, len = files.length; i<len; i++){
if(b.items){ if(b.items){
b.items.add(files[i]) b.items.add(files[i])
}else{ }else{
b.files[i] = files[i]; b.files[i] = files[i];
} }
} }
return b.files return b.files
} }
function change_files_to_file_field(file_field,files){ function change_files_to_file_field(file_field,files){
var fileupload = $(file_field).parents(".fileupload"); var fileupload = $(file_field).parents(".fileupload");
if(fileupload.length > 0){ if(fileupload.length > 0){
fileupload.find(".fileupload-preview").text(files[0].name); fileupload.find(".fileupload-preview").text(files[0].name);
} }
var files_list = new FileListItems(files) var files_list = new FileListItems(files)
try{ try{
$(file_field)[0].files = files_list; $(file_field)[0].files = files_list;
}catch(e){console.log(e)} }catch(e){console.log(e)}
if($(file_field)[0].files.length == 0){ //Change failed if($(file_field)[0].files.length == 0){ //Change failed
var file_field_values = []; var file_field_values = [];
var file_reader = new FileReader(); var file_reader = new FileReader();
$("[name=\""+$(file_field)[0].name+"\"][type=\"hidden\"]").remove(); $("[name=\""+$(file_field)[0].name+"\"][type=\"hidden\"]").remove();
var hidden_input = $("<input type=\"hidden\" name=\""+$(file_field)[0].name+"\">"); var hidden_input = $("<input type=\"hidden\" name=\""+$(file_field)[0].name+"\">");
var hidden_input_values = []; var hidden_input_values = [];
$(file_field).after(hidden_input); $(file_field).after(hidden_input);
var files_list_length = files_list.length; var files_list_length = files_list.length;
for(var i = 0; i < files_list_length; i++){ for(var i = 0; i < files_list_length; i++){
var file = files_list[i]; var file = files_list[i];
$(file_field)[0].files[i] = file; $(file_field)[0].files[i] = file;
file_reader.readAsBinaryString(files_list[i]); file_reader.readAsBinaryString(files_list[i]);
file_reader.onload = (function(hidden_input,file,i,files_list_length) { file_reader.onload = (function(hidden_input,file,i,files_list_length) {
return function(e) { return function(e) {
var file_info = {}; var file_info = {};
file_info["name"] = file.name; file_info["name"] = file.name;
file_info["type"] = file.type; file_info["type"] = file.type;
if (file_reader.result) if (file_reader.result)
file_reader.content = file_reader.result; file_reader.content = file_reader.result;
file_info["content"] = e ? e.target.result : file_reader.content; file_info["content"] = e ? e.target.result : file_reader.content;
if(Array.isArray(hidden_input_values)){ if(Array.isArray(hidden_input_values)){
hidden_input_values.push(file_info); hidden_input_values.push(file_info);
} }
if(i == files_list_length - 1){ if(i == files_list_length - 1){
if(hidden_input_values.length == 1){ if(hidden_input_values.length == 1){
hidden_input_values = hidden_input_values[0]; hidden_input_values = hidden_input_values[0];
} }
hidden_input.val(JSON.stringify(hidden_input_values)); hidden_input.val(JSON.stringify(hidden_input_values));
} }
};})(hidden_input,file,i,files_list_length); };})(hidden_input,file,i,files_list_length);
file_field_values.push("C:\\fakepath\\" + files_list[i].name); file_field_values.push("C:\\fakepath\\" + files_list[i].name);
} }
Object.defineProperty($(file_field)[0].files, "length", { Object.defineProperty($(file_field)[0].files, "length", {
// only returns odd die sides // only returns odd die sides
get: function () { get: function () {
var length = 0; var length = 0;
while(this[length]){ while(this[length]){
length++; length++;
} }
return length; return length;
} }
}); });
Object.defineProperty($(file_field)[0], "value", { Object.defineProperty($(file_field)[0], "value", {
// only returns odd die sides // only returns odd die sides
get: function () { get: function () {
return (this.getAttribute('value') ? this.getAttribute('value') : ""); return (this.getAttribute('value') ? this.getAttribute('value') : "");
}, },
set: function(value) { set: function(value) {
this.setAttribute('value',value); this.setAttribute('value',value);
} }
}); });
$(file_field)[0].value = file_field_values.join(", "); $(file_field)[0].value = file_field_values.join(", ");
} }
} }
function dragOverHandler(ev) { function dragOverHandler(ev) {
document.activeElement.blur(); document.activeElement.blur();
$(ev.target).addClass("in"); $(ev.target).addClass("in");
// Prevent default behavior (Prevent file from being opened) // Prevent default behavior (Prevent file from being opened)
ev.preventDefault(); ev.preventDefault();
} }
function dropHandler(ev) { function dropHandler(ev) {
window.ev = ev; window.ev = ev;
window.dataTransfer = ev.dataTransfer; window.dataTransfer = ev.dataTransfer;
// Prevent default behavior (Prevent file from being opened) // Prevent default behavior (Prevent file from being opened)
ev.preventDefault(); ev.preventDefault();
var files = []; var files = [];
if (ev.dataTransfer.items) { if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s) // Use DataTransferItemList interface to access the file(s)
for (var i = 0; i < ev.dataTransfer.items.length; i++) { for (var i = 0; i < ev.dataTransfer.items.length; i++) {
// If dropped items aren't files, reject them // If dropped items aren't files, reject them
if (ev.dataTransfer.items[i].kind === 'file') { if (ev.dataTransfer.items[i].kind === 'file') {
var file = ev.dataTransfer.items[i].getAsFile(); var file = ev.dataTransfer.items[i].getAsFile();
files.push(file) files.push(file)
} }
} }
} else { } else {
// Use DataTransfer interface to access the file(s) // Use DataTransfer interface to access the file(s)
for (var i = 0; i < ev.dataTransfer.files.length; i++) { for (var i = 0; i < ev.dataTransfer.files.length; i++) {
var file = ev.dataTransfer.files[i]; var file = ev.dataTransfer.files[i];
files.push(file) files.push(file)
} }
} }
files.forEach(function(file){ files.forEach(function(file){
var single_file = [file]; var single_file = [file];
var file_field = add_file_field(); var file_field = add_file_field();
change_files_to_file_field(file_field,single_file); change_files_to_file_field(file_field,single_file);
}) })
window.files = files; window.files = files;
var target = ev.target ? ev.target : ev.srcElement; var target = ev.target ? ev.target : ev.srcElement;
$(target).removeClass("in"); $(target).removeClass("in");
} }
function add_file_field(){ function add_file_field(){
var self = $('#add_file'); var self = $('#add_file');
var new_id = $(self).prev().attr('value'); var new_id = $(self).prev().attr('value');
var old_id = new RegExp("new_column_entry_files", "g"); var old_id = new RegExp("new_column_entry_files", "g");
var on = $('.language-nav li.active').index(); var on = $('.language-nav li.active').index();
var le = $('#add-target').children('.start-line').length; var le = $('#add-target').children('.start-line').length;
$(self).prev().attr('value', parseInt(new_id) + 1); $(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)); $('#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\"]"); 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() { $('#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'); $(self).children('.tab-pane').eq(on).addClass('in active').siblings().removeClass('in active');
}); });
formTip(); formTip();
return file_field; return file_field;
} }
$(document).ready(function() { $(document).ready(function() {
$(".plugin-sortable").sortable({ $(".plugin-sortable").sortable({
update : function(event, ui){ update : function(event, ui){
var existingfiles = $(".exist.plugin-sortable div.fileupload") var existingfiles = $(".exist.plugin-sortable div.fileupload")
existingfiles.each(function(i, file){ existingfiles.each(function(i, file){
$(file).find("input.file-sort-number-field").val(existingfiles.length - i); $(file).find("input.file-sort-number-field").val(existingfiles.length - i);
}) })
} }
}); });
$('.main-forms .add-on').tooltip(); $('.main-forms .add-on').tooltip();
$(document).on('click', '#add_file', add_file_field); $(document).on('click', '#add_file', add_file_field);
$(document).on('click', '.delete_file', function(){ $(document).on('click', '.delete_file', function(){
$(this).parents('.input-prepend').remove(); $(this).parents('.input-prepend').remove();
}); });
$(document).on('click',"[type='file']",function(){ $(document).on('click',"[type='file']",function(){
$("[name=\""+$(this).attr("name")+"\"][type=\"hiiden\"]").remove(); $("[name=\""+$(this).attr("name")+"\"][type=\"hiiden\"]").remove();
}); });
$(document).on('click', '.remove_existing_record', function(){ $(document).on('click', '.remove_existing_record', function(){
if(confirm("<%= I18n.t(:sure?)%>")){ if(confirm("<%= I18n.t(:sure?)%>")){
$(this).children('.should_destroy').attr('value', 1); $(this).children('.should_destroy').attr('value', 1);
$(this).parents('.start-line').hide(); $(this).parents('.start-line').hide();
} }
}); });
}); });
</script> </script>
<% end %> <% 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? %> <% if form_file.new_record? %>
<div class="fileupload fileupload-new start-line" data-provides="fileupload"> <div class="fileupload fileupload-new start-line" data-provides="fileupload">
<% else %> <% else %>
<div class="fileupload fileupload-exists start-line" data-provides="fileupload"> <div class="fileupload fileupload-exists start-line" data-provides="fileupload">
<i class="icons-list-2 sort-order-icon"></i> <i class="icons-list-2 sort-order-icon"></i>
<% if form_file.file.blank? %> <% if form_file.file.blank? %>
<%= t(:no_file) %> <%= t(:no_file) %>
<% else %> <% 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} %> <%= 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 %>
<% end %> <% end %>
<div class="input-prepend input-append"> <div class="input-prepend input-append">
<label> <label>
<span class="add-on btn btn-file" title="<%= t(:file_) %>"> <span class="add-on btn btn-file" title="<%= t(:file_) %>">
<i class="icons-paperclip"></i> <i class="icons-paperclip"></i>
<%= f.file_field :file %> <%= f.file_field :file %>
</span> </span>
<div class="uneditable-input input-medium"> <div class="uneditable-input input-medium">
<i class="icon-file fileupload-exists"></i> <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> <span class="fileupload-preview"><%= (form_file.new_record? || form_file.file.blank?) ? t(:select_file) : t(:change_file) %></span>
</div> </div>
</label> </label>
<span class="add-on icons-pencil" title="<%= t('file.name') %>"></span> <span class="add-on icons-pencil" title="<%= t('file.name') %>"></span>
<span class="tab-content"> <span class="tab-content">
<% @site_in_use_locales.each_with_index do |locale, i| %> <% @site_in_use_locales.each_with_index do |locale, i| %>
<span class="tab-pane fade <%= ( i == 0 ) ? "in active" : '' %> <%= locale %>"> <span class="tab-pane fade <%= ( i == 0 ) ? "in active" : '' %> <%= locale %>">
<%= f.fields_for :file_title_translations do |f| %> <%= 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) %> <%= f.text_field locale, :class => "input-medium", placeholder: t('file.name'), :value => (form_file.file_title_translations[locale] rescue nil) %>
<% end %> <% end %>
</span> </span>
<% end %> <% end %>
</span> </span>
<span class="add-on btn-group btn" title="<%= t('universal_table.show_lang') %>"> <span class="add-on btn-group btn" title="<%= t('universal_table.show_lang') %>">
<i class="icons-earth"></i> <span class="caret"></span> <i class="icons-earth"></i> <span class="caret"></span>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<% @site_in_use_locales.each do |locale| %> <% @site_in_use_locales.each do |locale| %>
<li> <li>
<label class="checkbox"> <label class="checkbox">
<%= check_box_tag "#{f.object_name}[choose_lang][]", locale, form_file.choose_lang.include?(locale.to_s) %> <%= check_box_tag "#{f.object_name}[choose_lang][]", locale, form_file.choose_lang.include?(locale.to_s) %>
<%= t(locale.to_s) %> <%= t(locale.to_s) %>
</label> </label>
</li> </li>
<% end %> <% end %>
</ul> </ul>
<%= hidden_field_tag "#{f.object_name}[choose_lang][]", '' %> <%= hidden_field_tag "#{f.object_name}[choose_lang][]", '' %>
</span> </span>
<% if form_file.new_record? %> <% if form_file.new_record? %>
<span class="delete_file add-on btn" title="<%= t(:delete_) %>"> <span class="delete_file add-on btn" title="<%= t(:delete_) %>">
<a class="icon-trash"></a> <a class="icon-trash"></a>
<%= f.hidden_field :sort_number, :value => "new_column_entry_file_sort_order_XXX", :class => "input-mini" %> <%= f.hidden_field :sort_number, :value => "new_column_entry_file_sort_order_XXX", :class => "input-mini" %>
</span> </span>
<% else %> <% else %>
<span class="remove_existing_record add-on btn" title="<%= t(:remove) %>"> <span class="remove_existing_record add-on btn" title="<%= t(:remove) %>">
<%= f.hidden_field :id %> <%= f.hidden_field :id %>
<%= f.hidden_field :sort_number , :class => "file-sort-number-field" %> <%= f.hidden_field :sort_number , :class => "file-sort-number-field" %>
<a class=" icon-remove"></a> <a class=" icon-remove"></a>
<%= f.hidden_field :_destroy, :value => nil, :class => 'should_destroy' %> <%= f.hidden_field :_destroy, :value => nil, :class => 'should_destroy' %>
</span> </span>
<span class="downloaded_times">Downloaded <b><%= form_file.download_count %></b> time<%= form_file.download_count > 1 ? "s" : "" %>.</span> <span class="downloaded_times">Downloaded <b><%= form_file.download_count %></b> time<%= form_file.download_count > 1 ? "s" : "" %>.</span>
<% end %> <% end %>
</div> </div>
</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"> <div class="control-group">
<%= f.label :image, column.title, :class => "control-label" %> <%= f.label :image, column.title, :class => "control-label" %>
<div class="controls"> <div class="controls">
<div class="fileupload fileupload-new clearfix <%= 'fileupload-edit' if image_field.image.file %>" data-provides="fileupload"> <div class="fileupload fileupload-new clearfix <%= 'fileupload-edit' if image_field.image.file %>" data-provides="fileupload">
<div class="fileupload-new thumbnail pull-left"> <div class="fileupload-new thumbnail pull-left">
<% if image_field.image.file %> <% if image_field.image.file %>
<%= image_tag image_field.image %> <%= image_tag image_field.image %>
<% else %> <% else %>
<img src="http://www.placehold.it/50x50/EFEFEF/AAAAAA" /> <img src="http://www.placehold.it/50x50/EFEFEF/AAAAAA" />
<% end %> <% end %>
</div> </div>
<div class="fileupload-preview fileupload-exists thumbnail pull-left"></div> <div class="fileupload-preview fileupload-exists thumbnail pull-left"></div>
<span class="btn btn-file"> <span class="btn btn-file">
<span class="fileupload-new"><%= t(:select_image) %></span> <span class="fileupload-new"><%= t(:select_image) %></span>
<span class="fileupload-exists"><%= t(:change) %></span> <span class="fileupload-exists"><%= t(:change) %></span>
<%= f.file_field :image %> <%= f.file_field :image %>
</span> </span>
<a href="#" class="btn fileupload-exists" data-dismiss="fileupload"><%= t(:cancel) %></a> <a href="#" class="btn fileupload-exists" data-dismiss="fileupload"><%= t(:cancel) %></a>
<div class="controls" data-toggle="buttons-checkbox"> <div class="controls" data-toggle="buttons-checkbox">
<label class="checkbox inline btn btn-danger fileupload-remove"> <label class="checkbox inline btn btn-danger fileupload-remove">
<%= f.check_box :remove_image %><%= t(:remove) %> <%= f.check_box :remove_image %><%= t(:remove) %>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<% if !image_field.new_record? %> <% if !image_field.new_record? %>
<%= f.hidden_field :id %> <%= f.hidden_field :id %>
<% else %> <% else %>
<%= f.hidden_field :table_column_id, :value => column.id %> <%= f.hidden_field :table_column_id, :value => column.id %>
<% end %> <% end %>
</div> </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"> <table class="table main-list">
<thead> <thead>
<tr class="sort-header"> <tr class="sort-header">
<% @table_fields.each do |f| %> <% @table_fields.each do |f| %>
<%= thead(f) %> <%= thead(f) %>
<% end %> <% end %>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% @tables.each do |table| %> <% @tables.each do |table| %>
<% can_edit = can_edit_or_delete?(table) %> <% can_edit = can_edit_or_delete?(table) %>
<tr id="table_<%= table.id.to_s %>"> <tr id="table_<%= table.id.to_s %>">
<td> <td>
<a href="<%= admin_universal_table_path(table) %>"><%= table.title %></a> <a href="<%= admin_universal_table_path(table) %>"><%= table.title %></a>
<div class="quick-edit"> <div class="quick-edit">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<% if can_edit %> <% if can_edit %>
<li><a href="<%= edit_admin_universal_table_path(table) %>"><%= t(:edit) %></a></li> <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> <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 %> <% if table.ordered_with_sort_number %>
<li><a href="<%= admin_universal_table_edit_sort_path(table) %>"><%= t('universal_table.edit_sort') %></a></li> <li><a href="<%= admin_universal_table_edit_sort_path(table) %>"><%= t('universal_table.edit_sort') %></a></li>
<% end %> <% 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_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> <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 %> <% end %>
</ul> </ul>
</div> </div>
</td> </td>
<td> <td>
<%= table.created_at.strftime("%Y-%m-%d") %> <%= table.created_at.strftime("%Y-%m-%d") %>
</td> </td>
<td> <td>
<%= table.table_entries.count %> <%= table.table_entries.count %>
</td> </td>
<td> <td>
<% if can_edit %> <% if can_edit %>
<form action="/admin/universal_tables/import_data_from_excel" method="post" enctype="multipart/form-data" class="import_from_excel_form"> <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 %> <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
<input type="file" name="import_data" /> <input type="file" name="import_data" />
<button class="btn btn-primary btn-small"><i class="icons-upload"></i></button> <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 %>" /> <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> <a href="<%= admin_universal_table_export_structure_path(table, :format => "xlsx") %>"><%= t("universal_table.export_structure") %></a>
</form> </form>
<% end %> <% end %>
</td> </td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>

View File

@ -1,25 +1,25 @@
<div class="control-group"> <div class="control-group">
<%= f.label :number, column.title, :class => "control-label" %> <%= f.label :number, column.title, :class => "control-label" %>
<div class="controls"> <div class="controls">
<div class="input-append"> <div class="input-append">
<%= f.number_field :number, :value => integer_field.number %> <%= f.number_field :number, :value => integer_field.number %>
</div> </div>
<% if column.make_categorizable %> <% if column.make_categorizable %>
<%= render_unique_number(f,column,i).html_safe %> <%= render_unique_number(f,column,i).html_safe %>
<script type="text/javascript"> <script type="text/javascript">
$("select#<%= column.key + "_" + i.to_s %>").on("change",function(){ $("select#<%= column.key + "_" + i.to_s %>").on("change",function(){
var el = $(this), var el = $(this),
value = el.val(), value = el.val(),
inputs = el.parent().parent().find("input[type=number]"); inputs = el.parent().parent().find("input[type=number]");
inputs.val(value); inputs.val(value);
}) })
</script> </script>
<% end %> <% end %>
</div> </div>
<% if !integer_field.new_record? %> <% if !integer_field.new_record? %>
<%= f.hidden_field :id %> <%= f.hidden_field :id %>
<% else %> <% else %>
<%= f.hidden_field :table_column_id, :value => column.id %> <%= f.hidden_field :table_column_id, :value => column.id %>
<% end %> <% end %>
</div> </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">
<div class="control-group ut-control-group-col"> <div class="control-group ut-control-group-col">
<%= f.label :period_from, column.title, :class => "control-label" %> <%= f.label :period_from, column.title, :class => "control-label" %>
<div class="controls"> <div class="controls">
<div> <div>
<div class="default_picker input-append" style=""> <div class="default_picker input-append" style="">
<% v = !period_field.new_record? ? format_date(period_field.period_from, column.date_format, true) : "" %> <% 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" %> <%= 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 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> <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> </div>
</div> </div>
<div class="control-group ut-control-group-col ut-control-group-col-right"> <div class="control-group ut-control-group-col ut-control-group-col-right">
<%= f.label :period_to, "~", :class => "control-label" %> <%= f.label :period_to, "~", :class => "control-label" %>
<div class="controls"> <div class="controls">
<div> <div>
<div class="default_picker input-append" style=""> <div class="default_picker input-append" style="">
<% v = !period_field.new_record? ? format_date(period_field.period_to, column.date_format, true) : "" %> <% 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" %> <%= 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 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> <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> </div>
</div> </div>
<% if !period_field.new_record? %> <% if !period_field.new_record? %>
<%= f.hidden_field :id %> <%= f.hidden_field :id %>
<% else %> <% else %>
<%= f.hidden_field :table_column_id, :value => column.id %> <%= f.hidden_field :table_column_id, :value => column.id %>
<% end %> <% end %>
</div> </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 %> <% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %> <%= stylesheet_link_tag "universal_table/universal-table" %>
<% end %> <% end %>
<% content_for :page_specific_javascript do %> <% content_for :page_specific_javascript do %>
<%= javascript_include_tag "universal_table/jquery-ui.min" %> <%= javascript_include_tag "universal_table/jquery-ui.min" %>
<% end %> <% end %>
<fieldset class="utable-heading-wrap"> <fieldset class="utable-heading-wrap">
<div class="utable-heading-header"> <div class="utable-heading-header">
<h4><%= t("universal_table.table_name") %></h4> <h4><%= t("universal_table.table_name") %></h4>
</div> </div>
<div class="control-group"> <div class="control-group">
<div class="controls"> <div class="controls">
<div class="input-append"> <div class="input-append">
<div class="tab-content"> <div class="tab-content">
<% @site_in_use_locales.each do |locale| %> <% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %> <% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<div class="tab-pane fade <%= active %>" id="table_name_<%= locale.to_s %>"> <div class="tab-pane fade <%= active %>" id="table_name_<%= locale.to_s %>">
<%= f.fields_for :title_translations do |f| %> <%= f.fields_for :title_translations do |f| %>
<%= f.text_field locale, :placeholder => "Title", :value => @table.title_translations[locale] %> <%= f.text_field locale, :placeholder => "Title", :value => @table.title_translations[locale] %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
<div class="btn-group" data-toggle="buttons-radio"> <div class="btn-group" data-toggle="buttons-radio">
<% @site_in_use_locales.each do |locale| %> <% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active" : "") %> <% 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"}%> <%= link_to t(locale).to_s,"#table_name_#{locale.to_s}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%>
<% end %> <% end %>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<div class="utable-heading-header"> <div class="utable-heading-header">
<h4><%= t("universal_table.default_ordered_field") %></h4> <h4><%= t("universal_table.default_ordered_field") %></h4>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label muted" for=""><%= t('universal_table.created_at') %></label> <label class="control-label muted" for=""><%= t('universal_table.created_at') %></label>
<div class="controls"> <div class="controls">
<div> <div>
<%= f.check_box :ordered_with_created_at, class: 'default_ordered_field ordered_with_created_at' %> <%= f.check_box :ordered_with_created_at, class: 'default_ordered_field ordered_with_created_at' %>
</div> </div>
<div class="order_direction<%= ' hidden' if !f.object.ordered_with_created_at %>"> <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]} %> <%= f.select :created_at_order_direction,['desc','asc'].map{|v| [t("universal_table.#{v}"),v]} %>
</div> </div>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label muted" for=""><%= t('universal_table.sort_number') %></label> <label class="control-label muted" for=""><%= t('universal_table.sort_number') %></label>
<div class="controls"> <div class="controls">
<div> <div>
<%= f.check_box :ordered_with_sort_number, class: 'default_ordered_field' %> <%= f.check_box :ordered_with_sort_number, class: 'default_ordered_field' %>
</div> </div>
<div class="order_direction<%= ' hidden' if !f.object.ordered_with_sort_number %>"> <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]} %> <%= f.select :sort_number_order_direction,['desc','asc'].map{|v| [t("universal_table.#{v}"),v]} %>
</div> </div>
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset class="utable-content"> <fieldset class="utable-content">
<div id="attributes-area" class="input-area"> <div id="attributes-area" class="input-area">
<% @table.table_columns.asc(:order).each_with_index do |table_column, index| %> <% @table.table_columns.asc(:order).each_with_index do |table_column, index| %>
<div class="attributes default "> <div class="attributes default ">
<%= f.fields_for :table_columns, table_column, :child_index => index.to_s do |f| %> <%= 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} %> <%= render :partial => "column", :object => table_column, :locals => {:f => f, :i => index} %>
<%= f.hidden_field :id %> <%= f.hidden_field :id %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-success add-attributes"><%= t("universal_table.add_column") %></button> <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") %>"> <input class="btn btn-primary" name="commit" type="submit" value="<%= t("save") %>">
</div> </div>
</fieldset> </fieldset>
<script type="text/javascript"> <script type="text/javascript">
var columnArea = $("#attributes-area"), var columnArea = $("#attributes-area"),
totalColumns = columnArea.find(".attributes.default").length, totalColumns = columnArea.find(".attributes.default").length,
columnCounter = totalColumns; columnCounter = totalColumns;
$("button.add-attributes").on("click",function(){ $("button.add-attributes").on("click",function(){
var html = "<%= escape_javascript(add_attribute 'column', f, :table_columns) %>", var html = "<%= escape_javascript(add_attribute 'column', f, :table_columns) %>",
replaceReg = new RegExp("new_table_columns","g"), replaceReg = new RegExp("new_table_columns","g"),
idNumber = new RegExp("XXX","g"); idNumber = new RegExp("XXX","g");
html = html.replace(replaceReg,columnCounter); html = html.replace(replaceReg,columnCounter);
html = html.replace("ColumnXX", "Column " + (columnCounter + 1)); html = html.replace("ColumnXX", "Column " + (columnCounter + 1));
html = html.replace(idNumber,columnCounter); html = html.replace(idNumber,columnCounter);
columnArea.append(html); columnArea.append(html);
columnCounter++; columnCounter++;
}) })
$(document.body).on("click","a.delete",function(){ $(document.body).on("click","a.delete",function(){
if($(this).parent().find(".attribute_field_to_delete").length == 0){ if($(this).parent().find(".attribute_field_to_delete").length == 0){
$(this).parent().parent().slideUp(function(){ $(this).parent().parent().slideUp(function(){
$(this).remove(); $(this).remove();
updateOrder(); updateOrder();
}); });
}else{ }else{
if(confirm("Are you sure?")){ if(confirm("Are you sure?")){
$(this).parent().parent().slideUp(function(){ $(this).parent().parent().slideUp(function(){
updateOrder(); updateOrder();
}); });
$(this).parent().find(".attribute_field_to_delete").val("true"); $(this).parent().find(".attribute_field_to_delete").val("true");
} }
} }
return false; return false;
}) })
$(document.body).on("change","select.type-selector",function(){ $(document.body).on("change","select.type-selector",function(){
var el = $(this), var el = $(this),
label = el.parent().find("span.link_to_show"); label = el.parent().find("span.link_to_show");
if(el.val() == "text" || el.val() == "integer"){ if(el.val() == "text" || el.val() == "integer"){
label.removeClass("hide"); label.removeClass("hide");
}else{ }else{
label.addClass("hide"); label.addClass("hide");
label.find("input[type=checkbox]").prop("checked",false); label.find("input[type=checkbox]").prop("checked",false);
} }
label = el.parent().find("label.date_format"); label = el.parent().find("label.date_format");
if(el.val() == "date" || el.val() == "period"){ if(el.val() == "date" || el.val() == "period"){
label.removeClass("hide"); label.removeClass("hide");
}else{ }else{
label.addClass("hide"); label.addClass("hide");
} }
}) })
$(document).on('change','.default_ordered_field',function(){ $(document).on('change','.default_ordered_field',function(){
$('.order_direction').addClass('hidden'); $('.order_direction').addClass('hidden');
if ($(this).prop('checked')){ if ($(this).prop('checked')){
$('.default_ordered_field').not(this).prop('checked',false); $('.default_ordered_field').not(this).prop('checked',false);
$(this).parents('.controls').eq(0).find('.order_direction').removeClass('hidden'); $(this).parents('.controls').eq(0).find('.order_direction').removeClass('hidden');
} }
}); });
$(document).ready(function(){ $(document).ready(function(){
if ($('.ordered_with_created_at').prop('checked')){ if ($('.ordered_with_created_at').prop('checked')){
$('.default_ordered_field').not($('.ordered_with_created_at')[0]).prop('checked',false); $('.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'); $('.order_direction').not($('.ordered_with_created_at').eq(0).parents('.controls').eq(0).find('.order_direction')[0]).addClass('hidden');
} }
}) })
function key_on_blur() { function key_on_blur() {
$('input[data-type=key]').on('blur',function() { $('input[data-type=key]').on('blur',function() {
var index_this = $(this).parents('.attributes').index() var index_this = $(this).parents('.attributes').index()
console.log(index_this) console.log(index_this)
var input_this = parseInt($(this).val()) - 1 var input_this = parseInt($(this).val()) - 1
if (input_this > ($('#attributes-area>.attributes').length-1)){ if (input_this > ($('#attributes-area>.attributes').length-1)){
input_this = $('#attributes-area>.attributes').length-1 input_this = $('#attributes-area>.attributes').length-1
}else if (input_this < 0){ }else if (input_this < 0){
input_this = 0 input_this = 0
} }
if (index_this > input_this){ if (index_this > input_this){
$(this).parents('#attributes-area>.attributes').insertBefore($('#attributes-area>.attributes').eq(input_this)) $(this).parents('#attributes-area>.attributes').insertBefore($('#attributes-area>.attributes').eq(input_this))
} }
else if (index_this < input_this){ else if (index_this < input_this){
$(this).parents('#attributes-area>.attributes').insertAfter($('#attributes-area>.attributes').eq(input_this)) $(this).parents('#attributes-area>.attributes').insertAfter($('#attributes-area>.attributes').eq(input_this))
} }
update_key(this) update_key(this)
}); });
} }
function update_key(ele){ function update_key(ele){
var ui_child=$(ele).parents('#attributes-area').find('.attributes'); var ui_child=$(ele).parents('#attributes-area').find('.attributes');
for (var i=0;i<ui_child.length;i++){ for (var i=0;i<ui_child.length;i++){
var now_ele = ui_child.eq(i); var now_ele = ui_child.eq(i);
now_ele.find('input[data-type=key]').val(i+1); now_ele.find('input[data-type=key]').val(i+1);
} }
updateOrder(); updateOrder();
} }
$('#attributes-area').ready(function(){ $('#attributes-area').ready(function(){
$("#attributes-area").sortable({ $("#attributes-area").sortable({
update: function( event, ui ) { update: function( event, ui ) {
update_key($(ui.item[0]).find('input[data-type=key]')) update_key($(ui.item[0]).find('input[data-type=key]'))
} }
}); });
$("#attributes-area").on('change',key_on_blur) $("#attributes-area").on('change',key_on_blur)
key_on_blur() key_on_blur()
}) })
var updateOrder = function(){ var updateOrder = function(){
var attributes = $("#attributes-area").find(".attributes:visible"); var attributes = $("#attributes-area").find(".attributes:visible");
attributes.each(function(i){ attributes.each(function(i){
$(this).find("input.order-hidden-field").val(i); $(this).find("input.order-hidden-field").val(i);
}) })
} }
</script> </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"> <div class="control-group">
<%= f.label :text, column.title, :class => "control-label" %> <%= f.label :text, column.title, :class => "control-label" %>
<div class="controls"> <div class="controls">
<div class="input-append"> <div class="input-append">
<div class="tab-content"> <div class="tab-content">
<% @site_in_use_locales.each do |locale| %> <% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %> <% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<% id = "table_entry_column_entries_#{i}_text_translations_#{locale.to_s}" %> <% id = "table_entry_column_entries_#{i}_text_translations_#{locale.to_s}" %>
<div class="tab-pane fade in <%= active %>" id="<%= id %>"> <div class="tab-pane fade in <%= active %>" id="<%= id %>">
<%= f.fields_for :text_translations do |f| %> <%= f.fields_for :text_translations do |f| %>
<%= f.text_field locale, :value => text_field.text_translations[locale.to_s], :for => locale.to_s %> <%= f.text_field locale, :value => text_field.text_translations[locale.to_s], :for => locale.to_s %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
<div class="btn-group" data-toggle="buttons-radio"> <div class="btn-group" data-toggle="buttons-radio">
<% @site_in_use_locales.each do |locale| %> <% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active" : "") %> <% active = (locale == @site_in_use_locales.first ? "active" : "") %>
<% id = "table_entry_column_entries_#{i}_text_translations_#{locale.to_s}" %> <% id = "table_entry_column_entries_#{i}_text_translations_#{locale.to_s}" %>
<%= link_to t(locale).to_s,"##{id}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%> <%= link_to t(locale).to_s,"##{id}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%>
<% end %> <% end %>
</div> </div>
</div> </div>
<% if column.make_categorizable %> <% if column.make_categorizable %>
<%= render_unique_texts(f,column,i).html_safe %> <%= render_unique_texts(f,column,i).html_safe %>
<script type="text/javascript"> <script type="text/javascript">
$("select#<%= column.key + "_" + i.to_s %>").on("change",function(){ $("select#<%= column.key + "_" + i.to_s %>").on("change",function(){
var el = $(this), var el = $(this),
locales = <%= @site_in_use_locales.to_json.html_safe %>, locales = <%= @site_in_use_locales.to_json.html_safe %>,
values = JSON.parse(el.val()), values = JSON.parse(el.val()),
inputs = el.parent().parent().find("input[type=text]"); inputs = el.parent().parent().find("input[type=text]");
$.each(locales,function(i,locale){ $.each(locales,function(i,locale){
var input = inputs.filter("input[for=" + locale + "]"); var input = inputs.filter("input[for=" + locale + "]");
input.val(values[locale]); input.val(values[locale]);
}) })
}) })
</script> </script>
<% end %> <% end %>
</div> </div>
<% if !text_field.new_record? %> <% if !text_field.new_record? %>
<%= f.hidden_field :id %> <%= f.hidden_field :id %>
<% else %> <% else %>
<%= f.hidden_field :table_column_id, :value => column.id %> <%= f.hidden_field :table_column_id, :value => column.id %>
<% end %> <% end %>
</div> </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| %> <%= form_for @table, url: admin_universal_table_path(@table), html: {class: "form-horizontal main-forms"} do |f| %>
<%= render :partial => "table_form", locals: {f: f} %> <%= render :partial => "table_form", locals: {f: f} %>
<% end %> <% 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| %> <%= form_for @entry, url: "/admin/universal_tables/update_entry", html: {class: "form-horizontal main-forms"} do |f| %>
<fieldset> <fieldset>
<%= f.hidden_field_tag 'id', f.object.id %> <%= f.hidden_field_tag 'id', f.object.id %>
<%= f.hidden_field_tag 'page', params[:page] %> <%= f.hidden_field_tag 'page', params[:page] %>
<%= render :partial => "entry_form", :locals => {:f => f} %> <%= render :partial => "entry_form", :locals => {:f => f} %>
</fieldset> </fieldset>
<% end %> <% 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 %> <% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %> <%= stylesheet_link_tag "universal_table/universal-table" %>
<% end %> <% end %>
<div style="margin-bottom: 1em;"> <div style="margin-bottom: 1em;">
<button type="button" class="btn btn-primary" id="update_sort_button"><%= t('universal_table.manual_update_sort') %></button> <button type="button" class="btn btn-primary" id="update_sort_button"><%= t('universal_table.manual_update_sort') %></button>
</div> </div>
<%= render partial: 'edit_sort' %> <%= render partial: 'edit_sort' %>
<script type="text/javascript"> <script type="text/javascript">
function update_sort(){ function update_sort(){
var ids = $.map($('#sortable>tr'),function(v){return $(v).data('id')}); var ids = $.map($('#sortable>tr'),function(v){return $(v).data('id')});
$.ajax({ $.ajax({
url: "<%= admin_universal_table_update_sort_path(@table) %>", url: "<%= admin_universal_table_update_sort_path(@table) %>",
type: 'POST', type: 'POST',
dataType: 'text', dataType: 'text',
data: {ids: ids}, data: {ids: ids},
success: function(data){ success: function(data){
$('#data-table').replaceWith(data); $('#data-table').replaceWith(data);
sortable(); sortable();
} }
}); });
} }
function sortable(){ function sortable(){
$( "#sortable" ).sortable({ $( "#sortable" ).sortable({
update: function( event, ui ) { update: function( event, ui ) {
update_sort(); update_sort();
} }
}); });
$('.sort_number').change(function(){ $('.sort_number').change(function(){
var new_sort_number = parseFloat($(this).val()); var new_sort_number = parseFloat($(this).val());
var min_number = $('.sort_number').length; var min_number = $('.sort_number').length;
var max_number = 0; var max_number = 0;
var pool = $('.sort_number').not(this); var pool = $('.sort_number').not(this);
var same_order = pool.filter(function(){ var same_order = pool.filter(function(){
var tmp_sort = parseFloat($(this).val()); var tmp_sort = parseFloat($(this).val());
if (tmp_sort<min_number){ if (tmp_sort<min_number){
min_number = tmp_sort; min_number = tmp_sort;
} }
if (tmp_sort>max_number){ if (tmp_sort>max_number){
max_number = tmp_sort; max_number = tmp_sort;
} }
return tmp_sort==new_sort_number return tmp_sort==new_sort_number
}); });
var tmp_same_order = null; var tmp_same_order = null;
if (same_order.length>0){ if (same_order.length>0){
tmp_same_order = same_order.eq(0); tmp_same_order = same_order.eq(0);
tmp_same_order.parents('tr').eq(0).before($(this).parents('tr').eq(0)); tmp_same_order.parents('tr').eq(0).before($(this).parents('tr').eq(0));
}else{ }else{
//ex. 1 2 3 5,insert 4 //ex. 1 2 3 5,insert 4
if (parseInt(pool.eq(0).val())<parseInt(pool.eq(-1).val())){ //asc if (parseInt(pool.eq(0).val())<parseInt(pool.eq(-1).val())){ //asc
$.each(pool,function(){ $.each(pool,function(){
var tmp_sort = parseFloat($(this).val()); var tmp_sort = parseFloat($(this).val());
if (new_sort_number>max_number){ if (new_sort_number>max_number){
if (tmp_sort<=new_sort_number){ if (tmp_sort<=new_sort_number){
tmp_same_order = $(this); tmp_same_order = $(this);
} }
}else{ }else{
if (tmp_sort>=new_sort_number){ if (tmp_sort>=new_sort_number){
tmp_same_order = $(this); tmp_same_order = $(this);
return false; return false;
} }
} }
}); });
if (new_sort_number>max_number){ if (new_sort_number>max_number){
tmp_same_order.parents('tr').eq(0).after($(this).parents('tr').eq(0)); tmp_same_order.parents('tr').eq(0).after($(this).parents('tr').eq(0));
}else{ }else{
tmp_same_order.parents('tr').eq(0).before($(this).parents('tr').eq(0)); tmp_same_order.parents('tr').eq(0).before($(this).parents('tr').eq(0));
} }
}else{ //desc }else{ //desc
//ex. 5 3 2 1,insert 4 //ex. 5 3 2 1,insert 4
$.each(pool,function(){ $.each(pool,function(){
var tmp_sort = parseFloat($(this).val()); var tmp_sort = parseFloat($(this).val());
if (new_sort_number<min_number){ if (new_sort_number<min_number){
if (tmp_sort>=new_sort_number){ if (tmp_sort>=new_sort_number){
tmp_same_order = $(this); tmp_same_order = $(this);
} }
}else if (new_sort_number>max_number){ }else if (new_sort_number>max_number){
if (tmp_sort<=new_sort_number){ if (tmp_sort<=new_sort_number){
tmp_same_order = $(this); tmp_same_order = $(this);
return false; return false;
} }
}else{ }else{
if (tmp_sort<=new_sort_number){ if (tmp_sort<=new_sort_number){
tmp_same_order = $(this); tmp_same_order = $(this);
return false; return false;
} }
} }
}); });
if (new_sort_number<min_number){ if (new_sort_number<min_number){
tmp_same_order.parents('tr').eq(0).after($(this).parents('tr').eq(0)); tmp_same_order.parents('tr').eq(0).after($(this).parents('tr').eq(0));
}else{ }else{
tmp_same_order.parents('tr').eq(0).before($(this).parents('tr').eq(0)); tmp_same_order.parents('tr').eq(0).before($(this).parents('tr').eq(0));
} }
} }
} }
update_sort(); update_sort();
}); });
} }
$(document).ready(function(){ $(document).ready(function(){
$('#update_sort_button').click(update_sort); $('#update_sort_button').click(update_sort);
sortable(); sortable();
}); });
</script> </script>

View File

@ -3,63 +3,71 @@
wb = xlsx_package.workbook wb = xlsx_package.workbook
wb.add_worksheet(name: "Structure") do |sheet| wb.add_worksheet(name: "Structure") do |sheet|
heading = sheet.styles.add_style(:b => true, :locked => true) heading = sheet.styles.add_style(b: true, locked: true)
type = sheet.styles.add_style(:i => true) type = sheet.styles.add_style(i: true)
row = [] row = ['UID']
row1 = [] row1 = ['uid']
row2 = [] row2 = ['Use to update existing entries. Leave blank to create new.']
@table.table_columns.asc(:order).each do |column| @table.table_columns.asc(:order).each do |column|
case column.type case column.type
when "text" when "text", "editor"
@site_in_use_locales.sort.each do |locale| @site_in_use_locales.sort.each do |locale|
row << column.title + " - " + t(locale.to_s) row << "#{column.title} - #{t(locale.to_s)}"
row1 << column.key row1 << column.key
row2 << column.type + "-#{locale}" row2 << "#{column.type}-#{locale}"
end 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
row << t("universal_table.hashtags") when "integer"
row1 << "table_tags" row << column.title
row2 << "Separate tags by ;" row1 << column.key
row2 << "integer"
row << t("universal_table.related_entries") when "image"
row1 << "related_entries" row << column.title
row2 << "Separate UIDs with ;" row1 << column.key
row2 << "Public URL"
sheet.add_row row, :style => heading when "date"
sheet.add_row row1 row << column.title
sheet.add_row row2, :style => type 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 %> <% content_for :page_specific_javascript do %>
<%= javascript_include_tag "lib/jquery.form" %> <%= javascript_include_tag "lib/jquery.form" %>
<% end %> <% end %>
<div id="index_table"> <div id="index_table">
<%= render 'index'%> <%= render 'index'%>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
$("form.import_from_excel_form").on("submit",function(){ $("form.import_from_excel_form").on("submit",function(){
var form = this; var form = this;
if($(this).find("input[type=file]").val() != ""){ if($(this).find("input[type=file]").val() != ""){
$(this).ajaxSubmit({ $(this).ajaxSubmit({
dataType : "json", dataType : "json",
success : function(data){ success : function(data){
if(data.success){ if(data.success){
alert("Import successfull.") alert("Import successfull.")
$("tr#table_" + data.id + " td:eq(2)").text(data.count); $("tr#table_" + data.id + " td:eq(2)").text(data.count);
}else{ }else{
alert(data.msg); alert(data.msg);
} }
form.reset(); form.reset();
} }
}) })
} }
return false; return false;
}) })
</script> </script>
<div id="downloadModal" data-backdrop="static" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="downloadModalLabel" aria-hidden="true"> <div id="downloadModal" data-backdrop="static" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="downloadModalLabel" aria-hidden="true">
<div class="modal-header"> <div class="modal-header">
<h3 id="downloadModalLabel">Download</h3> <h3 id="downloadModalLabel">Download</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p id="wait-zone" style="text-align: center;"> <p id="wait-zone" style="text-align: center;">
Please wait while we prepare your download. This may take a while. Please wait while we prepare your download. This may take a while.
<br /> <br />
<img src="/assets/spin.gif" /> <img src="/assets/spin.gif" />
</p> </p>
<p id="link-zone" style="display: none; text-align: center;"> <p id="link-zone" style="display: none; text-align: center;">
Please click the link below to download. Please click the link below to download.
<br /> <br />
<a href="" id="download-link" target="_blank">Download</a> <a href="" id="download-link" target="_blank">Download</a>
</p> </p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn" id="modal-close-btn" style="display:none;" data-dismiss="modal" aria-hidden="true">Close</button> <button class="btn" id="modal-close-btn" style="display:none;" data-dismiss="modal" aria-hidden="true">Close</button>
</div> </div>
</div> </div>
<script type="text/javascript" src="/assets/lib/process.manager.js"></script> <script type="text/javascript" src="/assets/lib/process.manager.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var downloadModal = $("#downloadModal"), var downloadModal = $("#downloadModal"),
checkForThread = null, checkForThread = null,
waitZone = $("#wait-zone"), waitZone = $("#wait-zone"),
linkZone = $("#link-zone"), linkZone = $("#link-zone"),
downloadLink = $("a#download-link"), downloadLink = $("a#download-link"),
modalBtn = $("#modal-close-btn"), modalBtn = $("#modal-close-btn"),
processManager = new ProcessManager(); processManager = new ProcessManager();
$(document).on("click", ".export-xls", function(){ $(document).on("click", ".export-xls", function(){
var link = $(this).attr("href"), var link = $(this).attr("href"),
title = null, title = null,
id = $(this).data("table-id"); id = $(this).data("table-id");
linkZone.hide(); linkZone.hide();
waitZone.show(); waitZone.show();
modalBtn.hide(); modalBtn.hide();
$.ajax({ $.ajax({
url : link, url : link,
type : "get", type : "get",
dataType : "json" dataType : "json"
}).done(function(data){ }).done(function(data){
title = data.title; title = data.title;
checkForThread = new Process(function(){ checkForThread = new Process(function(){
$.ajax({ $.ajax({
url : "/admin/universal_tables/checkforthread", url : "/admin/universal_tables/checkforthread",
type : "get", type : "get",
data : {"utable_id" : id, "utable_title" : title}, data : {"utable_id" : id, "utable_title" : title},
dataType : "json" dataType : "json"
}).done(function(data){ }).done(function(data){
if(!data.status){ if(!data.status){
downloadLink.attr("href", "/uploads/utable_export/" + id + "/" + title + ".xlsx"); downloadLink.attr("href", "/uploads/utable_export/" + id + "/" + title + ".xlsx");
waitZone.hide(); waitZone.hide();
linkZone.show(); linkZone.show();
modalBtn.show(); modalBtn.show();
checkForThread.kill(); checkForThread.kill();
} }
}) })
}) })
checkForThread.setTimeInterval(1000); checkForThread.setTimeInterval(1000);
checkForThread.setRepeat(Process.CONSTANTS.REPEAT_INFINITE); checkForThread.setRepeat(Process.CONSTANTS.REPEAT_INFINITE);
processManager.queue(checkForThread); processManager.queue(checkForThread);
}) })
downloadModal.modal("show"); downloadModal.modal("show");
return false; return false;
}) })
</script> </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| %> <%= form_for @table, url: admin_universal_tables_path, html: {class: "form-horizontal main-forms"} do |f| %>
<%= render :partial => "table_form", locals: {f: f} %> <%= render :partial => "table_form", locals: {f: f} %>
<% end %> <% 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| %> <%= form_for @entry, url: "/admin/universal_tables/add_entry", html: {class: "form-horizontal main-forms"} do |f| %>
<fieldset> <fieldset>
<%= f.hidden_field :u_table_id, :value => @table.id %> <%= f.hidden_field :u_table_id, :value => @table.id %>
<%= render :partial => "entry_form", :locals => {:f => f} %> <%= render :partial => "entry_form", :locals => {:f => f} %>
</fieldset> </fieldset>
<% end %> <% 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 %> <% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %> <%= stylesheet_link_tag "universal_table/universal-table" %>
<% end %> <% end %>
<style> <style>
#entry-status{ #entry-status{
float: right; float: right;
margin-right: 1em; margin-right: 1em;
} }
.toggle_entries[data-status="show"] { .toggle_entries[data-status="show"] {
border-radius: 4px; border-radius: 4px;
display: inline-block; display: inline-block;
padding: 4px 12px; padding: 4px 12px;
margin-bottom: 0; margin-bottom: 0;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
font-family: 'Varela Round'; font-family: 'Varela Round';
letter-spacing: -.4px; letter-spacing: -.4px;
color: #ffffff; color: #ffffff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
background-color: #007AFF; background-color: #007AFF;
margin-right: 0.5em; margin-right: 0.5em;
&:hover{ &:hover{
background-color:#1A73E8; background-color:#1A73E8;
} }
} }
.toggle_entries[data-status="hide"] { .toggle_entries[data-status="hide"] {
border-radius: 4px; border-radius: 4px;
display: inline-block; display: inline-block;
padding: 4px 12px; padding: 4px 12px;
margin-bottom: 0; margin-bottom: 0;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
font-family: 'Varela Round'; font-family: 'Varela Round';
letter-spacing: -.4px; letter-spacing: -.4px;
color: #ffffff; color: #ffffff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
background-color: #5F6368; background-color: #5F6368;
margin-right: 0.5em; margin-right: 0.5em;
&:hover{ &:hover{
background-color:#4b4e53; background-color:#4b4e53;
} }
} }
.hidden_entry{ .hidden_entry{
td{ td{
background-color: #9eacb5a8; background-color: #9eacb5a8;
} }
} }
</style> </style>
<form class="form-search" action="<%= admin_universal_table_path(@table) %>" method="get"> <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"> <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> <button type="submit" class="btn btn-primary">Search</button>
<% if params[:q].present? %> <% if params[:q].present? %>
<a href="<%= admin_universal_table_path(@table) %>" class="btn btn-info">Reset</a> <a href="<%= admin_universal_table_path(@table) %>" class="btn btn-info">Reset</a>
<% end %> <% end %>
</form> </form>
<div id="data-table" class="ut-table"> <div id="data-table" class="ut-table">
<table class="table main-list"> <table class="table main-list">
<thead> <thead>
<tr class="sort-header"> <tr class="sort-header">
<th width="15"> <th width="15">
<%= t(:status) %> <%= t(:status) %>
<div class="status hide" id="entry-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="hide"><%= t(:hide) %></a>
<a href="" class="toggle_entries" data-status="show"><%= t(:show) %></a> <a href="" class="toggle_entries" data-status="show"><%= t(:show) %></a>
</div> </div>
</th> </th>
<% @table_fields.each do |field| %> <% @table_fields.each do |field| %>
<% <%
sort = field.to_s.include?('.') ? field.to_s.split('.')[1] : field.to_s sort = field.to_s.include?('.') ? field.to_s.split('.')[1] : field.to_s
active = params[:sort].eql? sort active = params[:sort].eql? sort
order = active ? (["asc", "desc"]-[params[:order]]).first : "asc" 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>" arrow = (order.eql? "desc") ? "<b class='icons-arrow-up-3'></b>" : "<b class='icons-arrow-down-4'></b>"
klass = field.eql?(:title) ? "span5" : "span2" klass = field.eql?(:title) ? "span5" : "span2"
th_data = "<a href='?sort=#{sort}&order=#{order}'>#{field} #{active ? arrow : ""}</a>" th_data = "<a href='?sort=#{sort}&order=#{order}'>#{field} #{active ? arrow : ""}</a>"
%> %>
<th class='<%= klass %> <%= active ? "active" : "" %>'><%= th_data.html_safe %></th> <th class='<%= klass %> <%= active ? "active" : "" %>'><%= th_data.html_safe %></th>
<% end %> <% end %>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% can_edit = can_edit_or_delete?(@table) %> <% can_edit = can_edit_or_delete?(@table) %>
<% @entries.each do |entry| %> <% @entries.each do |entry| %>
<tr id="tr_entry_<%= entry.id.to_s %>" class="<%= entry.is_hidden? ? 'hidden_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> <td><input class="hide-toggle" type="checkbox" value="<%= entry.id.to_s %>" /></td>
<% @columns.each_with_index do |column, index| %> <% @columns.each_with_index do |column, index| %>
<% ce = entry.column_entries.where(:table_column_id => column.id).first rescue nil %> <% ce = entry.column_entries.where(:table_column_id => column.id).first rescue nil %>
<td> <td>
<% if !ce.nil? %> <% if !ce.nil? %>
<% case ce.type %> <% case ce.type %>
<% when "text" %> <% when "text" %>
<%= ce.text %> <%= ce.text %>
<% when "integer" %> <% when "integer" %>
<%= ce.number %> <%= ce.number %>
<% when "editor" %> <% when "editor" %>
<%= ce.content.html_safe rescue "" %> <%= ce.content.html_safe rescue "" %>
<% when "image" %> <% when "image" %>
<div class="image-expander"> <div class="image-expander">
<% if !ce.image.nil? %> <% if !ce.image.nil? %>
<a href="<%= ce.image.url %>" target="_blank"><img src="<%= ce.image.thumb.url %>" class="image-preview" /></a> <a href="<%= ce.image.url %>" target="_blank"><img src="<%= ce.image.thumb.url %>" class="image-preview" /></a>
<% end %> <% end %>
</div> </div>
<% when "date" %> <% when "date" %>
<%= format_date(ce.date, column.date_format) %> <%= format_date(ce.date, column.date_format) %>
<% when "period" %> <% when "period" %>
<% if !ce.period_from.nil? %> <% if !ce.period_from.nil? %>
<%= format_date(ce.period_from, column.date_format) %> ~ <%= format_date(ce.period_to, column.date_format) %> <%= format_date(ce.period_from, column.date_format) %> ~ <%= format_date(ce.period_to, column.date_format) %>
<% end %> <% end %>
<% when "file" %> <% when "file" %>
<% locale = I18n.locale.to_s %> <% locale = I18n.locale.to_s %>
<ol> <ol>
<% ce.column_entry_files.desc(:sort_number).each do |entry_file| %> <% ce.column_entry_files.desc(:sort_number).each do |entry_file| %>
<% next unless entry_file.choose_lang_display(locale) %> <% next unless entry_file.choose_lang_display(locale) %>
<% if entry_file.file.content_type.start_with?('audio/') %> <% if entry_file.file.content_type.start_with?('audio/') %>
<%= entry_file.get_file_title %> <%= 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> <a class="voice-player" data-content="<%= entry_file.file.url %>" href="" title="播放讀音"><i class="fa fa-play" aria-hidden="true"></i></a>
<% else %> <% else %>
<li><%= link_to entry_file.get_file_title, entry_file.file.url, target: "_blank" %></li> <li><%= link_to entry_file.get_file_title, entry_file.file.url, target: "_blank" %></li>
<% end %> <% end %>
<% end %> <% end %>
</ol> </ol>
<% end %> <% end %>
<% else %> <% else %>
&nbsp; &nbsp;
<% end %> <% end %>
<% if index == 0 && can_edit %> <% if index == 0 && can_edit %>
<div class="quick-edit"> <div class="quick-edit">
<ul class="nav nav-pills"> <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_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> <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> </ul>
</div> </div>
<% end %> <% end %>
</td> </td>
<% end %> <% end %>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="bottomnav clearfix"> <div class="bottomnav clearfix">
<%= content_tag :div, paginate(@entries), class: "pagination pagination-centered" %> <%= content_tag :div, paginate(@entries), class: "pagination pagination-centered" %>
<div class="action pull-right"> <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> <a href="<%= admin_universal_table_new_entry_path(@table) %>" class="btn btn-primary" role="button" data-toggle="modal">Add Entry</a>
</div> </div>
</div> </div>
<script> <script>
$(".action").after($("#entry-status")); $(".action").after($("#entry-status"));
let audio; let audio;
$(".voice-player").on("click", function(){ $(".voice-player").on("click", function(){
let status = $(this).attr('status'); let status = $(this).attr('status');
if (audio) { if (audio) {
audio.pause(); audio.pause();
audio.currentTime = 0; audio.currentTime = 0;
} }
if (status == 'playing') { if (status == 'playing') {
$(this).attr('status', ''); $(this).attr('status', '');
$(this).find('i').removeClass('fa-pause'); $(this).find('i').removeClass('fa-pause');
$(this).find('i').addClass('fa-play'); $(this).find('i').addClass('fa-play');
} else { } else {
let mp3_url = $(this).attr('data-content'); let mp3_url = $(this).attr('data-content');
let _this = $(this); let _this = $(this);
audio = new Audio(mp3_url); audio = new Audio(mp3_url);
audio.play(); audio.play();
audio.onended = function() { audio.onended = function() {
_this.attr('status', ''); _this.attr('status', '');
_this.find('i').removeClass('fa-pause'); _this.find('i').removeClass('fa-pause');
_this.find('i').addClass('fa-play'); _this.find('i').addClass('fa-play');
}; };
$(this).find('i').removeClass('fa-play'); $(this).find('i').removeClass('fa-play');
$(this).find('i').addClass('fa-pause'); $(this).find('i').addClass('fa-pause');
$(this).attr('status', 'playing'); $(this).attr('status', 'playing');
} }
return false; return false;
}) })
$(".hide-toggle").on("click", function(){ $(".hide-toggle").on("click", function(){
var count = $(".hide-toggle:checked").length; var count = $(".hide-toggle:checked").length;
if(count > 0){ if(count > 0){
$("#entry-status").removeClass("hide") $("#entry-status").removeClass("hide")
}else{ }else{
$("#entry-status").addClass("hide"); $("#entry-status").addClass("hide");
} }
}) })
$(".toggle_entries").on("click", function(){ $(".toggle_entries").on("click", function(){
let checkedValues = $('.hide-toggle:checked').map(function() { let checkedValues = $('.hide-toggle:checked').map(function() {
return $(this).val(); return $(this).val();
}).get(); }).get();
let status = $(this).data("status"); let status = $(this).data("status");
$.ajax({ $.ajax({
url: "/admin/universal_tables/toggle_entries", url: "/admin/universal_tables/toggle_entries",
method: "post", method: "post",
data: {"ids": checkedValues, "status": status}, data: {"ids": checkedValues, "status": status},
type: "json" type: "json"
}).done(function(){ }).done(function(){
if(status == "hide"){ if(status == "hide"){
$('.hide-toggle:checked').parents("tr").addClass("hidden_entry"); $('.hide-toggle:checked').parents("tr").addClass("hidden_entry");
}else{ }else{
$('.hide-toggle:checked').parents("tr").removeClass("hidden_entry"); $('.hide-toggle:checked').parents("tr").removeClass("hidden_entry");
} }
}) })
return false; return false;
}) })
</script> </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' %> <% if @ext == 'pdf' %>
<%= render partial: 'archives/viewer' %> <%= render partial: 'archives/viewer' %>
<% else %> <% else %>
<html lang="<%= I18n.locale.to_s%>" style="margin: 0em; padding: 0em; width: 100%; height: 100%; overflow: hidden; background-color: rgb(230, 230, 230);"> <html lang="<%= I18n.locale.to_s%>" style="margin: 0em; padding: 0em; width: 100%; height: 100%; overflow: hidden; background-color: rgb(230, 230, 230);">
<head> <head>
<meta name="viewport" content="width=device-width, minimum-scale=0.1"> <meta name="viewport" content="width=device-width, minimum-scale=0.1">
<title><%=@filename%></title> <title><%=@filename%></title>
<%= stylesheet_link_tag "archive/download_file.css" %> <%= stylesheet_link_tag "archive/download_file.css" %>
</head> </head>
<body> <body>
<h1 style="display: none;"><%=@filename%></h1> <h1 style="display: none;"><%=@filename%></h1>
<% if @ext != "png" && @ext != "jpg" && @ext != "bmp" %> <% if @ext != "png" && @ext != "jpg" && @ext != "bmp" %>
<object data="<%=@url%>" height="100%" type="application/<%=@ext%>" width="100%"> <object data="<%=@url%>" height="100%" type="application/<%=@ext%>" width="100%">
<iframe height="100%" src="<%=@url%>" title="<%=@filename%>" width="100%"></iframe> <iframe height="100%" src="<%=@url%>" title="<%=@filename%>" width="100%"></iframe>
<img alt="<%=@filename%>" src="<%=@url%>"> <img alt="<%=@filename%>" src="<%=@url%>">
</object> </object>
<% else %> <% else %>
<img alt="<%=@filename%>" src="<%=@url%>"> <img alt="<%=@filename%>" src="<%=@url%>">
<script type="text/javascript"> <script type="text/javascript">
var img = document.getElementsByTagName('img')[0]; var img = document.getElementsByTagName('img')[0];
var width = img.width; var width = img.width;
var height = img.height; var height = img.height;
window.innerWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; window.innerWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
window.innerHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; window.innerHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
var window_width = window.innerWidth; var window_width = window.innerWidth;
var window_height = window.innerHeight; var window_height = window.innerHeight;
var zoom_in_cursor,zoom_out_cursor; var zoom_in_cursor,zoom_out_cursor;
var IE_ver = 11; var IE_ver = 11;
if(navigator.userAgent.search("MSIE") != -1){ if(navigator.userAgent.search("MSIE") != -1){
IE_ver = Number(navigator.userAgent.split("MSIE")[1].split(";")[0]); IE_ver = Number(navigator.userAgent.split("MSIE")[1].split(";")[0]);
} }
if(IE_ver <= 8){ if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px"; img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px"; img.style.marginLeft = "-"+img.width/2+"px";
}else{ }else{
img.style.transform= "translate(-50%, -50%)"; img.style.transform= "translate(-50%, -50%)";
img.style["-ms-transform"]= "translate(-50%, -50%)"; img.style["-ms-transform"]= "translate(-50%, -50%)";
img.style["-moz-transform"]= "translate(-50%, -50%)"; img.style["-moz-transform"]= "translate(-50%, -50%)";
} }
if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) { if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) {
zoom_in_cursor = 'url("/assets/archive/zoomin.cur"), auto'; zoom_in_cursor = 'url("/assets/archive/zoomin.cur"), auto';
zoom_out_cursor = 'url("/assets/archive/zoomout.cur"), auto'; zoom_out_cursor = 'url("/assets/archive/zoomout.cur"), auto';
}else{ }else{
zoom_in_cursor = 'zoom-in'; zoom_in_cursor = 'zoom-in';
zoom_out_cursor = 'zoom-out'; zoom_out_cursor = 'zoom-out';
} }
if(height > window_height && (height / width) > (window_height / window_width) ){ if(height > window_height && (height / width) > (window_height / window_width) ){
img.height = window_height; img.height = window_height;
img.width = window_height / height * width; img.width = window_height / height * width;
img.style.cursor = zoom_in_cursor; img.style.cursor = zoom_in_cursor;
if(IE_ver <= 8){ if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px"; img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px"; img.style.marginLeft = "-"+img.width/2+"px";
} }
img.onclick=function(e){ img.onclick=function(e){
var event = e || window.event; var event = e || window.event;
if(img.style.cursor == zoom_in_cursor){ if(img.style.cursor == zoom_in_cursor){
var cursor_x = event.clientX; var cursor_x = event.clientX;
var cursor_y = event.clientY; var cursor_y = event.clientY;
img.height = height; img.height = height;
img.width = width; img.width = width;
img.style.cursor = zoom_out_cursor; img.style.cursor = zoom_out_cursor;
document.getElementsByTagName('html')[0].style.overflow = ""; document.getElementsByTagName('html')[0].style.overflow = "";
img.style.transform= "none"; img.style.transform= "none";
img.style["-ms-transform"]= "none"; img.style["-ms-transform"]= "none";
img.style["-moz-transform"]= "none"; img.style["-moz-transform"]= "none";
img.style.top= "0"; img.style.top= "0";
img.style.left= "0"; img.style.left= "0";
if(IE_ver <= 8){ if(IE_ver <= 8){
img.style.marginTop = "0"; img.style.marginTop = "0";
img.style.marginLeft = "0"; img.style.marginLeft = "0";
} }
window.scroll( window.scroll(
(((cursor_x - (window_width - window_height / height * width)/2) * height / window_height) - window_width / 2), (((cursor_x - (window_width - window_height / height * width)/2) * height / window_height) - window_width / 2),
((cursor_y * height / window_height) - window_height / 2) ((cursor_y * height / window_height) - window_height / 2)
); );
}else{ }else{
img.height = window_height; img.height = window_height;
img.width = window_height / height * width; img.width = window_height / height * width;
img.style.cursor = zoom_in_cursor; img.style.cursor = zoom_in_cursor;
document.getElementsByTagName('html')[0].style.overflow = "hidden"; document.getElementsByTagName('html')[0].style.overflow = "hidden";
if(IE_ver <= 8){ if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px"; img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px"; img.style.marginLeft = "-"+img.width/2+"px";
}else{ }else{
img.style.transform= "translate(-50%, -50%)"; img.style.transform= "translate(-50%, -50%)";
img.style["-ms-transform"]= "translate(-50%, -50%)"; img.style["-ms-transform"]= "translate(-50%, -50%)";
img.style["-moz-transform"]= "translate(-50%, -50%)"; img.style["-moz-transform"]= "translate(-50%, -50%)";
} }
img.style.top= "50%"; img.style.top= "50%";
img.style.left= "50%"; img.style.left= "50%";
window.scroll(0, 0); window.scroll(0, 0);
} }
}; };
}else if(width > window_width){ }else if(width > window_width){
img.width = window_width; img.width = window_width;
img.height = window_width / width * height; img.height = window_width / width * height;
img.style.cursor = zoom_in_cursor; img.style.cursor = zoom_in_cursor;
if(IE_ver <= 8){ if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px"; img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px"; img.style.marginLeft = "-"+img.width/2+"px";
} }
img.onclick=function(e){ img.onclick=function(e){
var event = e || window.event; var event = e || window.event;
if(img.style.cursor == zoom_in_cursor){ if(img.style.cursor == zoom_in_cursor){
var cursor_x = event.clientX; var cursor_x = event.clientX;
var cursor_y = event.clientY; var cursor_y = event.clientY;
img.height = height; img.height = height;
img.width = width; img.width = width;
img.style.cursor = zoom_out_cursor; img.style.cursor = zoom_out_cursor;
document.getElementsByTagName('html')[0].style.overflow = ""; document.getElementsByTagName('html')[0].style.overflow = "";
img.style.transform= "none"; img.style.transform= "none";
img.style["-ms-transform"]= "none"; img.style["-ms-transform"]= "none";
img.style["-moz-transform"]= "none"; img.style["-moz-transform"]= "none";
img.style.top= "0"; img.style.top= "0";
img.style.left= "0"; img.style.left= "0";
if(IE_ver <= 8){ if(IE_ver <= 8){
img.style.marginTop = "0"; img.style.marginTop = "0";
img.style.marginLeft = "0"; img.style.marginLeft = "0";
} }
window.scroll( ((cursor_x * height / window_height) - window_width / 2), 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) (((cursor_y - (window_height - window_width / width * height)/2) * height / window_height) - window_height / 2)
); );
}else{ }else{
img.width = window_width; img.width = window_width;
img.height = window_width / width * height; img.height = window_width / width * height;
img.style.cursor = zoom_in_cursor; img.style.cursor = zoom_in_cursor;
document.getElementsByTagName('html')[0].style.overflow = "hidden"; document.getElementsByTagName('html')[0].style.overflow = "hidden";
if(IE_ver <= 8){ if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px"; img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px"; img.style.marginLeft = "-"+img.width/2+"px";
}else{ }else{
img.style.transform= "translate(-50%, -50%)"; img.style.transform= "translate(-50%, -50%)";
img.style["-ms-transform"]= "translate(-50%, -50%)"; img.style["-ms-transform"]= "translate(-50%, -50%)";
img.style["-moz-transform"]= "translate(-50%, -50%)"; img.style["-moz-transform"]= "translate(-50%, -50%)";
} }
img.style.top= "50%"; img.style.top= "50%";
img.style.left= "50%"; img.style.left= "50%";
window.scroll(0, 0); window.scroll(0, 0);
} }
}; };
} }
</script> </script>
<% end %> <% end %>
</body> </body>
</html> </html>
<% end %> <% end %>

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

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

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

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

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

@ -1,64 +1,64 @@
<% <%
data = action_data data = action_data
OrbitHelper.render_css_in_head(["mind_map/mindmap"]) OrbitHelper.render_css_in_head(["mind_map/mindmap"])
%> %>
<h2><%= data["title"] %></h2> <h2><%= data["title"] %></h2>
<div id="jsmind_container"></div> <div id="jsmind_container"></div>
<script type="module"> <script type="module">
import '/assets/mind_map/utils/custom.overrides.js' import '/assets/mind_map/utils/custom.overrides.js'
import '/assets/mind_map/jsmind/plugins/jsmind.draggable-node.js' import '/assets/mind_map/jsmind/plugins/jsmind.draggable-node.js'
import { initJsmind, getJsmindData } from '/assets/mind_map/utils/custom.main.js' import { initJsmind, getJsmindData } from '/assets/mind_map/utils/custom.main.js'
import { INITIAL_MIND } from '/assets/mind_map/utils/custom.config.js' import { INITIAL_MIND } from '/assets/mind_map/utils/custom.config.js'
// 操控心智圖是否可編輯 // 操控心智圖是否可編輯
// Control whether the mind map is editable // Control whether the mind map is editable
let isEditable = false let isEditable = false
// 心智圖實例 // 心智圖實例
// Mind map instance // Mind map instance
let jm let jm
// 心智圖初始數據 // 心智圖初始數據
// Initial mind map data // Initial mind map data
let mind = { let mind = {
meta: {}, meta: {},
format: 'node_array', format: 'node_array',
data: <%= raw data["mind_map_data"].to_json %> data: <%= raw data["mind_map_data"].to_json %>
} }
// 心智圖自訂選項(可參考 jsmind 官方文檔) // 心智圖自訂選項(可參考 jsmind 官方文檔)
// Custom options for the mind map (refer to the jsmind official documentation) // Custom options for the mind map (refer to the jsmind official documentation)
const options = { const options = {
container: 'jsmind_container', container: 'jsmind_container',
support_html: true, support_html: true,
editable: isEditable, editable: isEditable,
theme: 'primary', theme: 'primary',
mode: 'full', mode: 'full',
tableUID: '', tableUID: '',
text: { text: {
addNode: "<%= t("universal_table.add_node") %>", addNode: "<%= t("universal_table.add_node") %>",
deleteNode: "<%= t("universal_table.delete_node") %>", deleteNode: "<%= t("universal_table.delete_node") %>",
strokeColor: "<%= t("universal_table.stroke_color") %>", strokeColor: "<%= t("universal_table.stroke_color") %>",
bgColor: "<%= t("universal_table.bg_color") %>", bgColor: "<%= t("universal_table.bg_color") %>",
textColor: "<%= t("universal_table.text_color") %>" textColor: "<%= t("universal_table.text_color") %>"
}, },
view: { view: {
engine: 'svg', engine: 'svg',
draggable: true, draggable: true,
node_overflow: 'wrap', node_overflow: 'wrap',
}, },
shortcut: { shortcut: {
mapping: { mapping: {
// 避免與 Toolbar 按下 Enter 事件衝突 // 避免與 Toolbar 按下 Enter 事件衝突
// Avoid conflicts with the Enter key event in the Toolbar // Avoid conflicts with the Enter key event in the Toolbar
addbrother: 2048 + 13, addbrother: 2048 + 13,
}, },
}, },
} }
// 初始化心智圖並掛載實例 // 初始化心智圖並掛載實例
// Initialize the mind map and attach the instance // Initialize the mind map and attach the instance
jm = initJsmind(mind, options, isEditable) jm = initJsmind(mind, options, isEditable)
</script> </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%>"> <html lang="<%= I18n.locale.to_s%>">
<head> <head>
<title><%=@filename%></title> <title><%=@filename%></title>
<link href="/assets/archive/download_file.css" rel="stylesheet" media="all"> <link href="/assets/archive/download_file.css" rel="stylesheet" media="all">
</head> </head>
<body style="background: #fff;"> <body style="background: #fff;">
<div class="wrap_block"> <div class="wrap_block">
<h1 style="display: none;"><%=@filename%></h1> <h1 style="display: none;"><%=@filename%></h1>
<% download_text = t('download') + " " + @filename %> <% download_text = t('download') + " " + @filename %>
<h2 align="center"><a href="<%=@url%>" target="blank" download="<%=@filename%>" title="<%=download_text%>"><%=download_text%></a></h2> <h2 align="center"><a href="<%=@url%>" target="blank" download="<%=@filename%>" title="<%=download_text%>"><%=download_text%></a></h2>
<p align="center"> <p align="center">
<a href="javascript:window.close();" title="<%=t('close')%>"><%=t('close')%></a> <a href="javascript:window.close();" title="<%=t('close')%>"><%=t('close')%></a>
</p> </p>
</div> </div>
<script>window.location.href="<%=@url%>";</script> <script>window.location.href="<%=@url%>";</script>
</body> </body>
</html> </html>

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

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

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

@ -3,127 +3,132 @@
wb = xlsx_package.workbook wb = xlsx_package.workbook
wb.add_worksheet(name: "Structure") do |sheet| wb.add_worksheet(name: "Structure") do |sheet|
heading = sheet.styles.add_style(:b => true, :locked => true) heading = sheet.styles.add_style(b: true, locked: true)
type = sheet.styles.add_style(:i => true) type = sheet.styles.add_style(i: true)
wrap = sheet.styles.add_style alignment: {wrap_text: true} wrap = sheet.styles.add_style alignment: { wrap_text: true }
row = [] row = []
row1 = [] row1 = []
row2 = [] row2 = []
row << "UID" row << "UID"
row1 << "uid" row1 << "uid"
row2 << "uid" row2 << "uid"
table.table_columns.asc(:order).each do |column| table.table_columns.asc(:order).each do |column|
case column.type case column.type
when "text" when "text", "editor"
site_in_use_locales.sort.each do |locale| site_in_use_locales.sort.each do |locale|
row << column.title + " - " + t(locale.to_s) row << "#{column.title} - #{t(locale.to_s)}"
row1 << column.key row1 << column.key
row2 << column.type + "-#{locale}" row2 << "#{column.type}-#{locale}"
end end
when "integer" when "integer"
row << column.title row << column.title
row1 << column.key row1 << column.key
row2 << column.type row2 << column.type
when "editor" when "image"
site_in_use_locales.sort.each do |locale| row << column.title
row << column.title + " - " + t(locale.to_s) row1 << column.key
row1 << column.key row2 << "Public URL"
row2 << column.type + "-#{locale}" when "date"
end row << column.title
when "image" row1 << column.key
row << column.title row2 << "#{column.type} : #{column.date_format.upcase}"
row1 << column.key when "period"
row2 << "Public URL" row << "#{column.title} - From ~ To"
when "date" row1 << column.key
row << column.title row2 << "#{column.type} : #{column.date_format.upcase}-period_from ~ period_to"
row1 << column.key when "file"
row2 << column.type + " : " + column.date_format.upcase row << "#{column.title} (Link)"
when "period" row1 << column.key
row << column.title + "-From ~ To" row2 << "Separate the files by ;"
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
row << t("universal_table.hashtags") row << "#{column.title} 註解"
row1 << "table_tags" row1 << column.key
row2 << "Separate tags by ;" row2 << "file_title-#{locale}"
end
end
row << t("universal_table.related_entries") row << t("universal_table.hashtags")
row1 << "related_entries" row1 << "table_tags"
row2 << "Separate UIDs with ;" row2 << "Separate tags by ;"
sheet.add_row row, :style => heading row << t("universal_table.related_entries")
sheet.add_row row1 row1 << "related_entries"
sheet.add_row row2, :style => type row2 << "Separate UIDs with ;"
table.table_entries.asc(:created_at).each do |entry| sheet.add_row row, style: heading
row = [] sheet.add_row row1
row << entry.uid sheet.add_row row2, style: type
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
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 #!/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. # 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_ROOT = File.expand_path('../..', __FILE__)
ENGINE_PATH = File.expand_path('../../lib/universal_table/engine', __FILE__) ENGINE_PATH = File.expand_path('../../lib/universal_table/engine', __FILE__)
# Set up gems listed in the Gemfile. # Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
require 'rails/all' require 'rails/all'
require 'rails/engine/commands' require 'rails/engine/commands'

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

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

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

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

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

@ -1,45 +1,45 @@
Rails.application.routes.draw do Rails.application.routes.draw do
if ENV['worker_num']=='0' && File.basename($0) != 'rake' && !Rails.const_defined?('Console') if ENV['worker_num']=='0' && File.basename($0) != 'rake' && !Rails.const_defined?('Console')
Thread.new do Thread.new do
stored_flag = "utf1" stored_flag = "utf1"
need_update = Site.pluck(:tmp_flags).flatten.compact.exclude?(stored_flag) need_update = Site.pluck(:tmp_flags).flatten.compact.exclude?(stored_flag)
if need_update if need_update
TableEntry.all.to_a.each do |te| TableEntry.all.to_a.each do |te|
te.fix_have_data te.fix_have_data
end end
Site.update_all("$push"=>{"tmp_flags"=> stored_flag}) Site.update_all("$push"=>{"tmp_flags"=> stored_flag})
end end
end end
end end
locales = Site.first.in_use_locales rescue I18n.available_locales locales = Site.first.in_use_locales rescue I18n.available_locales
scope "(:locale)", locale: Regexp.new(locales.join("|")) do scope "(:locale)", locale: Regexp.new(locales.join("|")) do
namespace :admin do namespace :admin do
post "/universal_tables/add_entry", to: 'universal_tables#add_entry' post "/universal_tables/add_entry", to: 'universal_tables#add_entry'
post "/universal_tables/toggle_entries", to: 'universal_tables#toggle_entries' post "/universal_tables/toggle_entries", to: 'universal_tables#toggle_entries'
get "/universal_tables/get_entries", to: 'universal_tables#get_entries' get "/universal_tables/get_entries", to: 'universal_tables#get_entries'
get "/universal_tables/get_mindmaps", to: 'universal_tables#get_mindmaps' get "/universal_tables/get_mindmaps", to: 'universal_tables#get_mindmaps'
patch "/universal_tables/update_entry", to: 'universal_tables#update_entry' patch "/universal_tables/update_entry", to: 'universal_tables#update_entry'
post "/universal_tables/import_data_from_excel", to: 'universal_tables#import_data_from_excel' post "/universal_tables/import_data_from_excel", to: 'universal_tables#import_data_from_excel'
get "universal_tables/checkforthread", to: "universal_tables#checkforthread" get "universal_tables/checkforthread", to: "universal_tables#checkforthread"
get "/universal_table/:id/mind_maps", to: "mind_maps#index" get "/universal_table/:id/mind_maps", to: "mind_maps#index"
resources :universal_tables do resources :universal_tables do
get "new_entry" get "new_entry"
delete "delete_entry" delete "delete_entry"
get "edit_entry" get "edit_entry"
get "edit_sort" get "edit_sort"
post "update_sort", to: 'universal_tables#update_sort' post "update_sort", to: 'universal_tables#update_sort'
get "export_structure" get "export_structure"
member do member do
get "export_data" get "export_data"
end end
end end
resources :mind_maps resources :mind_maps
end end
get "/xhr/universal_table/export", to: 'universal_tables#export_filtered' get "/xhr/universal_table/export", to: 'universal_tables#export_filtered'
get "/xhr/universal_table/download", to: "universal_tables#download_file" get "/xhr/universal_table/download", to: "universal_tables#download_file"
end end
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" # desc "Explaining what the task does"
# task :universal_table do # task :universal_table do
# # Task goes here # # Task goes here
# end # end
namespace :universal_table_tasks do namespace :universal_table_tasks do
task :prepare_download,[:utable_id, :url] => :environment do |task,args| task :prepare_download,[:utable_id, :url] => :environment do |task,args|
id = args.utable_id id = args.utable_id
I18n.locale = :zh_tw I18n.locale = :zh_tw
table = UTable.find(id) table = UTable.find(id)
ac = ActionController::Base.new() ac = ActionController::Base.new()
host_url = Site.first.root_url host_url = Site.first.root_url
if host_url == "http://" if host_url == "http://"
host_url = "http://#{args.url}" host_url = "http://#{args.url}"
end 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} 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}" dirname = "public/uploads/utable_export/#{id}"
FileUtils.mkdir_p(dirname) unless File.exist?(dirname) FileUtils.mkdir_p(dirname) unless File.exist?(dirname)
f = "#{dirname}/#{table.title.gsub(/[ "'*@#$%^&()+=;:.,?>|\\\/<~_!:,、。!?;「」〈〉【】/]/,'')}.xlsx" f = "#{dirname}/#{table.title.gsub(/[ "'*@#$%^&()+=;:.,?>|\\\/<~_!:,、。!?;「」〈〉【】/]/,'')}.xlsx"
if File.exist?(f) if File.exist?(f)
File.delete(f) File.delete(f)
end end
file = File.open(f, "w") file = File.open(f, "w")
xlsx.force_encoding("utf-8") xlsx.force_encoding("utf-8")
file.write(xlsx) file.write(xlsx)
end end
end end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,33 +1,33 @@
<style> <style>
.universal-table-show { .universal-table-show {
border: 1px solid #eee; border: 1px solid #eee;
border-collapse: collapse; border-collapse: collapse;
} }
.universal-table-show.table td{ .universal-table-show.table td{
padding: 15px 18px; padding: 15px 18px;
} }
.table-title { .table-title {
border-right: 1px solid #eee; border-right: 1px solid #eee;
} }
</style> </style>
<table class="table table-striped universal-table-show"> <table class="table table-striped universal-table-show">
<tbody data-level="0" data-list="entry"> <tbody data-level="0" data-list="entry">
<tr> <tr>
<td class="col-md-2 table-title">{{title}}</td> <td class="col-md-2 table-title">{{title}}</td>
<td>{{text}}</td> <td>{{text}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="view_count pull-right"> <div class="view_count pull-right">
<i class="fa fa-eye">{{view_count_head}}:</i> <i class="fa fa-eye">{{view_count_head}}:</i>
<span class="view-count">{{view_count}}</span> <span class="view-count">{{view_count}}</span>
</div> </div>
<div data-list="related_entries" data-level="0"> <div data-list="related_entries" data-level="0">
<tbody data-level="1" data-list="related_entry"> <tbody data-level="1" data-list="related_entry">
<tr> <tr>
<td>{{text}}</td> <td>{{text}}</td>
</tr> </tr>
</tbody> </tbody>
</div> </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 == README
This README would normally document whatever steps are necessary to get the This README would normally document whatever steps are necessary to get the
application up and running. application up and running.
Things you may want to cover: Things you may want to cover:
* Ruby version * Ruby version
* System dependencies * System dependencies
* Configuration * Configuration
* Database creation * Database creation
* Database initialization * Database initialization
* How to run the test suite * How to run the test suite
* Services (job queues, cache servers, search engines, etc.) * Services (job queues, cache servers, search engines, etc.)
* Deployment instructions * Deployment instructions
* ... * ...
Please feel free to use a different markup language if you do not plan to run Please feel free to use a different markup language if you do not plan to run
<tt>rake doc:app</tt>. <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, # 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. # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require File.expand_path('../config/application', __FILE__) require File.expand_path('../config/application', __FILE__)
Rails.application.load_tasks 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 // This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below. // listed below.
// //
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, // 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. // 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 // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. // compiled file.
// //
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives. // about supported directives.
// //
//= require_tree . //= require_tree .

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