feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run

- Base: wintercms/winter branch 1.2 (full framework)
- Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS
- Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication
- Partials: hero (offline-first), features, modes (offline/nube toggle),
  screenshots, pricing (3 planes), comparison, FAQ, CTA
- Plugin VivesPOS.Site with ContactForm
- Dockerfile: PHP 8.2 Apache, port 80, healthcheck
- Added winter/wn-pages, blog, sitemap, seo plugins
- Active theme set to vivespos
This commit is contained in:
2026-08-21 19:29:00 -06:00
commit 1f72193a64
3266 changed files with 531480 additions and 0 deletions

View File

@@ -0,0 +1,421 @@
/*
* The autcomplete plugin, a forked version of Bootstrap's original typeahead plugin.
*
* Data attributes:
* - data-control="autocomplete" - enables the autocomplete plugin
*
* JavaScript API:
* $('input').autocomplete()
*
* Forked by daftspunk:
*
* - Source can be an object [{ value: 'something', label: 'Something' }, { value: 'else', label: 'Something Else' }]
* - Source can also be { something: 'Something', else: 'Else' }
*/
!function($){
"use strict"; // jshint ;_;
/* AUTOCOMPLETE PUBLIC CLASS DEFINITION
* ================================= */
var Autocomplete = function (element, options) {
this.$element = $(element)
this.options = $.extend({}, $.fn.autocomplete.defaults, options)
this.matcher = this.options.matcher || this.matcher
this.sorter = this.options.sorter || this.sorter
this.highlighter = this.options.highlighter || this.highlighter
this.updater = this.options.updater || this.updater
this.source = this.options.source
this.$menu = $(this.options.menu)
this.shown = false
this.listen()
}
Autocomplete.prototype = {
constructor: Autocomplete,
select: function () {
var val = this.$menu.find('.active').attr('data-value')
this.$element
.val(this.updater(val))
.change()
return this.hide()
},
updater: function (item) {
return item
},
show: function () {
var offset = this.options.bodyContainer ? this.$element.offset() : this.$element.position(),
pos = $.extend({}, offset, {
height: this.$element[0].offsetHeight
}),
cssOptions = {
top: pos.top + pos.height
, left: pos.left
}
if (this.options.matchWidth) {
cssOptions.width = this.$element[0].offsetWidth
}
this.$menu.css(cssOptions)
if (this.options.bodyContainer) {
$(document.body).append(this.$menu)
}
else {
this.$menu.insertAfter(this.$element)
}
this.$menu.show()
this.shown = true
return this
},
hide: function () {
this.$menu.hide()
this.shown = false
return this
},
lookup: function (event) {
var items
this.query = this.$element.val()
if (!this.query || this.query.length < this.options.minLength) {
return this.shown ? this.hide() : this
}
items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
return items ? this.process(items) : this
},
itemValue: function (item) {
if (typeof item === 'object')
return item.value;
return item;
},
itemLabel: function (item) {
if (typeof item === 'object')
return item.label;
return item;
},
itemsToArray: function (items) {
var newArray = []
$.each(items, function(value, label){
newArray.push({ label: label, value: value })
})
return newArray
},
process: function (items) {
var that = this
if (typeof items == 'object')
items = this.itemsToArray(items)
items = $.grep(items, function (item) {
return that.matcher(item)
})
items = this.sorter(items)
if (!items.length) {
return this.shown ? this.hide() : this
}
return this.render(items.slice(0, this.options.items)).show()
},
matcher: function (item) {
return ~this.itemValue(item).toLowerCase().indexOf(this.query.toLowerCase())
},
sorter: function (items) {
var beginswith = [],
caseSensitive = [],
caseInsensitive = [],
item,
itemValue
while (item = items.shift()) {
itemValue = this.itemValue(item)
if (!itemValue.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
else if (~itemValue.indexOf(this.query)) caseSensitive.push(item)
else caseInsensitive.push(item)
}
return beginswith.concat(caseSensitive, caseInsensitive)
},
highlighter: function (item) {
var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
return '<strong>' + match + '</strong>'
})
},
render: function (items) {
var that = this
items = $(items).map(function (i, item) {
i = $(that.options.item).attr('data-value', that.itemValue(item))
i.find('a').html(that.highlighter(that.itemLabel(item)))
return i[0]
})
items.first().addClass('active')
this.$menu.html(items)
return this
},
next: function (event) {
var active = this.$menu.find('.active').removeClass('active'),
next = active.next()
if (!next.length) {
next = $(this.$menu.find('li')[0])
}
next.addClass('active')
},
prev: function (event) {
var active = this.$menu.find('.active').removeClass('active'),
prev = active.prev()
if (!prev.length) {
prev = this.$menu.find('li').last()
}
prev.addClass('active')
},
listen: function () {
this.$element
.on('focus.autocomplete', $.proxy(this.focus, this))
.on('blur.autocomplete', $.proxy(this.blur, this))
.on('keypress.autocomplete', $.proxy(this.keypress, this))
.on('keyup.autocomplete', $.proxy(this.keyup, this))
if (this.eventSupported('keydown')) {
this.$element.on('keydown.autocomplete', $.proxy(this.keydown, this))
}
this.$menu
.on('click.autocomplete', $.proxy(this.click, this))
.on('mouseenter.autocomplete', 'li', $.proxy(this.mouseenter, this))
.on('mouseleave.autocomplete', 'li', $.proxy(this.mouseleave, this))
},
eventSupported: function(eventName) {
var isSupported = eventName in this.$element
if (!isSupported) {
this.$element.setAttribute(eventName, 'return;')
isSupported = typeof this.$element[eventName] === 'function'
}
return isSupported
},
move: function (e) {
if (!this.shown) return
switch(e.key) {
case 'Tab':
case 'Enter':
case 'Escape':
e.preventDefault()
break
case 'ArrowUp':
e.preventDefault()
this.prev()
break
case 'ArrowDown':
e.preventDefault()
this.next()
break
}
e.stopPropagation()
},
keydown: function (e) {
this.suppressKeyPressRepeat = ~$.inArray(e.key, ['ArrowDown','ArrowUp','Tab','Enter','Escape'])
this.move(e)
},
keypress: function (e) {
if (this.suppressKeyPressRepeat) return
this.move(e)
},
keyup: function (e) {
switch(e.keyCode) {
case 40: // down arrow
case 38: // up arrow
case 16: // shift
case 17: // ctrl
case 18: // alt
break
case 9: // tab
case 13: // enter
if (!this.shown) return
this.select()
break
case 27: // escape
if (!this.shown) return
this.hide()
break
default:
this.lookup()
}
e.stopPropagation()
e.preventDefault()
},
focus: function (e) {
this.focused = true
},
blur: function (e) {
this.focused = false
if (!this.mousedover && this.shown) this.hide()
},
click: function (e) {
e.stopPropagation()
e.preventDefault()
this.select()
this.$element.focus()
},
mouseenter: function (e) {
this.mousedover = true
this.$menu.find('.active').removeClass('active')
$(e.currentTarget).addClass('active')
},
mouseleave: function (e) {
this.mousedover = false
if (!this.focused && this.shown) this.hide()
},
destroy: function() {
this.hide()
this.$element.removeData('autocomplete')
this.$menu.remove()
this.$element.off('.autocomplete')
this.$menu.off('.autocomplete')
this.$element = null
this.$menu = null
}
}
/* AUTOCOMPLETE PLUGIN DEFINITION
* =========================== */
var old = $.fn.autocomplete
$.fn.autocomplete = function (option) {
return this.each(function () {
var $this = $(this)
, data = $this.data('autocomplete')
, options = typeof option == 'object' && option
if (!data) $this.data('autocomplete', (data = new Autocomplete(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.autocomplete.defaults = {
source: [],
items: 8,
menu: '<ul class="autocomplete dropdown-menu"></ul>',
item: '<li><a href="#"></a></li>',
minLength: 1,
bodyContainer: false
}
$.fn.autocomplete.Constructor = Autocomplete
/* AUTOCOMPLETE NO CONFLICT
* =================== */
$.fn.autocomplete.noConflict = function () {
$.fn.autocomplete = old
return this
}
/* AUTOCOMPLETE DATA-API
* ================== */
function paramToObj(name, value) {
if (value === undefined) value = ''
if (typeof value == 'object') return value
try {
return ocJSON("{" + value + "}")
}
catch (e) {
throw new Error('Error parsing the '+name+' attribute value. '+e)
}
}
$(document).on('focus.autocomplete.data-api', '[data-control="autocomplete"]', function (e) {
var $this = $(this)
if ($this.data('autocomplete')) return
var opts = $this.data()
if (opts.source) {
opts.source = paramToObj('data-source', opts.source)
}
$this.autocomplete(opts)
})
}(window.jQuery);
/* =============================================================
* bootstrap-autocomplete.js v2.3.1
* http://twitter.github.com/bootstrap/javascript.html#autocomplete
* =============================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ============================================================ */

View File

@@ -0,0 +1,81 @@
/*
* Callout
*
* - Documentation: ../docs/callout.md
*/
+function ($) {
'use strict';
// CALLOUT CLASS DEFINITION
// ======================
var dismiss = '[data-dismiss="callout"]'
var Callout = function (el) {
$(el).on('click', dismiss, this.close)
}
Callout.prototype.close = function (e) {
var $this = $(this)
var selector = $this.attr('data-target')
if (!selector) {
selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
var $parent = $(selector)
if (e) e.preventDefault()
if (!$parent.length) {
$parent = $this.hasClass('callout') ? $this : $this.parent()
}
$parent.trigger(e = $.Event('close.oc.callout'))
if (e.isDefaultPrevented()) return
$parent.removeClass('in')
function removeElement() {
$parent.trigger('closed.oc.callout').remove()
}
$.support.transition && $parent.hasClass('fade')
? $parent
.one($.support.transition.end, removeElement)
.emulateTransitionEnd(500)
: removeElement()
}
// CALLOUT PLUGIN DEFINITION
// =======================
var old = $.fn.callout
$.fn.callout = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.callout')
if (!data) $this.data('oc.callout', (data = new Callout(this)))
if (typeof option == 'string') data[option].call($this)
})
}
$.fn.callout.Constructor = Callout
// CALLOUT NO CONFLICT
// =================
$.fn.callout.noConflict = function () {
$.fn.callout = old
return this
}
// CALLOUT DATA-API
// ==============
$(document).on('click.oc.callout.data-api', dismiss, Callout.prototype.close)
}(jQuery);

View File

@@ -0,0 +1,138 @@
/*
* The bar chart plugin.
*
* Data attributes:
* - data-control="chart-bar" - enables the bar chart plugin
* - data-height="200" - optional, height of the graph
* - data-full-width="1" - optional, determines whether the chart should use the full width of the container
*
* JavaScript API:
* $('.scoreboard .chart').barChart()
*
* Dependences:
* - Raphaël (raphael-min.js)
*/
+function ($) { "use strict";
var BarChart = function (element, options) {
this.options = options || {}
var
$el = this.$el = $(element),
size = this.size = $el.height(),
self = this,
values = $.wn.chartUtils.loadListValues($('ul', $el)),
$legend = $.wn.chartUtils.createLegend($('ul', $el)),
indicators = $.wn.chartUtils.initLegendColorIndicators($legend),
isFullWidth = this.isFullWidth(),
chartHeight = this.options.height !== undefined ? this.options.height : size,
chartWidth = isFullWidth ? this.$el.width() : size,
barWidth = (chartWidth - (values.values.length-1)*this.options.gap)/values.values.length
var $canvas = $('<div/>').addClass('canvas').height(chartHeight).width(isFullWidth ? '100%' : chartWidth)
$el.prepend($canvas)
$el.toggleClass('full-width', isFullWidth)
Raphael($canvas.get(0), isFullWidth ? '100%' : chartWidth, chartHeight, function(){
self.paper = this
self.bars = this.set()
self.paper.customAttributes.bar = function (start, height) {
return {
path: [
["M", start, chartHeight],
["L", start, chartHeight-height],
["L", start + barWidth, chartHeight-height],
["L", start + barWidth, chartHeight],
["Z"]
]
}
}
// Add bars
var start = 0
$.each(values.values, function(index, valueInfo) {
var color = valueInfo.color !== undefined ? valueInfo.color : $.wn.chartUtils.getColor(index),
path = self.paper.path().attr({"stroke-width": 0}).attr({bar: [start, 0]}).attr({fill: color})
self.bars.push(path)
indicators[index].css('background-color', color)
start += barWidth + self.options.gap
path.hover(function(ev){
$.wn.chartUtils.showTooltip(ev.pageX, ev.pageY,
$.trim($.wn.chartUtils.getLegendLabel($legend, index)) + ': <strong>'+valueInfo.value+'</stong>')
}, function() {
$.wn.chartUtils.hideTooltip()
})
})
// Animate bars
start = 0
$.each(values.values, function(index, valueInfo) {
var height = (values.max && valueInfo.value) ? chartHeight/values.max * valueInfo.value : 0
self.bars[index].animate({bar: [start, height]}, 1000, "bounce")
start += barWidth + self.options.gap
})
// Update the full-width chart when the window is redized
if (isFullWidth) {
$(window).on('resize', function(){
chartWidth = self.$el.width()
barWidth = (chartWidth - (values.values.length-1)*self.options.gap)/values.values.length
var start = 0
$.each(values.values, function(index, valueInfo) {
var height = (values.max && valueInfo.value) ? chartHeight/values.max * valueInfo.value : 0
self.bars[index].animate({bar: [start, height]}, 10, "bounce")
start += barWidth + self.options.gap
})
})
}
})
}
BarChart.prototype.isFullWidth = function() {
return this.options.fullWidth !== undefined && this.options.fullWidth
}
BarChart.DEFAULTS = {
gap: 2
}
// BARCHART PLUGIN DEFINITION
// ============================
var old = $.fn.barChart
$.fn.barChart = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.barChart')
var options = $.extend({}, BarChart.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data)
$this.data('oc.barChart', new BarChart(this, options))
})
}
$.fn.barChart.Constructor = BarChart
// BARCHART NO CONFLICT
// =================
$.fn.barChart.noConflict = function () {
$.fn.barChart = old
return this
}
// BARCHART DATA-API
// ===============
$(document).render(function () {
$('[data-control=chart-bar]').barChart()
})
}(window.jQuery)

View File

@@ -0,0 +1,235 @@
/*
* Line Chart Plugin
*
* Data attributes:
* - data-control="chart-line" - enables the line chart plugin
* - data-reset-zoom-link="#reset-zoom" - specifies a link to reset zoom
* - data-zoomable - indicates that the chart is zoomable
* - data-time-mode="weeks" - if the "weeks" value is specified and the xaxis mode is "time", the X axis labels will be displayed as week end dates.
* - data-chart-options="xaxis: {mode: 'time'}" - specifies the Flot configuration in JSON format. See https://github.com/flot/flot/blob/master/API.md for details.
*
* Data sets are defined with the SPAN elements inside the chart element: <span data-chart="dataset" data-set-data="[0,0],[1,19]">
* Data set elements could contain data attributes with names in the format "data-set-color". The names for the data set
* attributes are described in the Flot documentation: https://github.com/flot/flot/blob/master/API.md#data-format
*
* JavaScript API:
* $('.chart').chartLine({ resetZoomLink:'#reset-zoom' })
*
* Dependences:
* - Flot (jquery.flot.js)
* - Flot Tooltip (jquery.flot.tooltip.js)
* - Flot Resize (jquery.flot.resize.js)
* - Flot Time (jquery.flot.time.js)
*/
+function ($) { "use strict";
// LINE CHART CLASS DEFINITION
// ============================
var ChartLine = function(element, options) {
var self = this
/*
* Flot options
*/
this.chartOptions = {
xaxis: {
mode: "time",
tickLength: 5
},
selection: { mode: "x" },
grid: {
markingsColor: "rgba(0,0,0, 0.02)",
backgroundColor: { colors: ["#fff", "#fff"] },
borderColor: "#7bafcc",
borderWidth: 0,
color: "#ddd",
hoverable: true,
clickable: true,
labelMargin: 10
},
series: {
lines: {
show: true,
fill: true
},
points: {
show: true
}
},
tooltip: true,
tooltipOpts: {
defaultTheme: false,
content: "%x: <strong>%y</strong>",
dateFormat: "%y-%0m-%0d",
shifts: {
x: 10,
y: 20
}
},
legend: {
show: true,
noColumns: 2
}
}
this.defaultDataSetOptions = {
shadowSize: 0
}
var parsedOptions = {}
try {
parsedOptions = ocJSON("{" + options.chartOptions + "}");
} catch (e) {
throw new Error('Error parsing the data-chart-options attribute value. '+e);
}
this.chartOptions = $.extend({}, this.chartOptions, parsedOptions)
this.options = options
this.$el = $(element)
this.fullDataSet = []
this.resetZoomLink = $(options.resetZoomLink)
this.$el.trigger('oc.chartLineInit', [this])
/*
* Bind Events
*/
this.resetZoomLink.on('click', $.proxy(this.clearZoom, this));
if (this.options.zoomable) {
this.$el.on("plotselected", function (event, ranges) {
var newCoords = {
xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }
}
$.plot(self.$el, self.fullDataSet, $.extend(true, {}, self.chartOptions, newCoords))
self.resetZoomLink.show()
});
}
/*
* Markings Helper
*/
if (this.chartOptions.xaxis.mode == "time" && this.options.timeMode == "weeks")
this.chartOptions.markings = weekendAreas
function weekendAreas(axes) {
var markings = [],
d = new Date(axes.xaxis.min);
// Go to the first Saturday
d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 1) % 7))
d.setUTCSeconds(0)
d.setUTCMinutes(0)
d.setUTCHours(0)
var i = d.getTime()
do {
// When we don't set yaxis, the rectangle automatically
// extends to infinity upwards and downwards
markings.push({ xaxis: { from: i, to: i + 2 * 24 * 60 * 60 * 1000 } })
i += 7 * 24 * 60 * 60 * 1000
} while (i < axes.xaxis.max)
return markings
}
/*
* Process the datasets
*/
this.initializing = true
this.$el.find('>[data-chart="dataset"]').each(function(){
var data = $(this).data(),
processedData = {};
for (var key in data) {
var normalizedKey = key.substring(3),
value = data[key];
normalizedKey = normalizedKey.charAt(0).toLowerCase() + normalizedKey.slice(1);
if (normalizedKey == 'data')
value = JSON.parse('['+value+']');
processedData[normalizedKey] = value;
}
self.addDataSet($.extend({}, self.defaultDataSetOptions, processedData));
})
/*
* Build chart
*/
this.initializing = false
this.rebuildChart()
}
ChartLine.DEFAULTS = {
chartOptions: "",
timeMode: null,
zoomable: false
}
/*
* Adds a data set to the chart.
* See https://github.com/flot/flot/blob/master/API.md#data-format for the list
* of supported data set options.
*/
ChartLine.prototype.addDataSet = function (dataSet) {
this.fullDataSet.push(dataSet)
if (!this.initializing)
this.rebuildChart()
}
ChartLine.prototype.rebuildChart = function() {
this.$el.trigger('oc.beforeChartLineRender', [this])
$.plot(this.$el, this.fullDataSet, this.chartOptions)
}
ChartLine.prototype.clearZoom = function() {
this.rebuildChart()
this.resetZoomLink.hide()
}
// LINE CHART PLUGIN DEFINITION
// ============================
var old = $.fn.chartLine
$.fn.chartLine = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('winter.chartLine')
var options = $.extend({}, ChartLine.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('winter.chartLine', (data = new ChartLine(this, options)))
if (typeof option == 'string') data[option].call($this)
})
}
$.fn.chartLine.Constructor = ChartLine
// LINE CHART NO CONFLICT
// =================
$.fn.chartLine.noConflict = function () {
$.fn.chartLine = old
return this
}
// LINE CHART DATA-API
// ===============
$(document).render(function () {
$('[data-control="chart-line"]').chartLine()
})
}(window.jQuery);

View File

@@ -0,0 +1,77 @@
/*
* The goal meter plugin.
*
* Applies the goal meter style to a scoreboard item.
*
* Data attributes:
* - data-control="goal-meter" - enables the goal meter plugin
* - data-value - sets the value, in percents
*
* JavaScript API:
* $('.scoreboard .goal-meter').goalMeter({value: 20})
* $('.scoreboard .goal-meter').goalMeter(10) // Sets the current value
*/
+function ($) { "use strict";
var GoalMeter = function (element, options) {
var
$el = this.$el = $(element),
self = this;
this.options = options || {};
this.$indicatorBar = $('<span/>').text(this.options.value + '%')
this.$indicatorOuter = $('<span/>').addClass('goal-meter-indicator').append(this.$indicatorBar)
$('p', this.$el).first().before(this.$indicatorOuter)
window.setTimeout(function(){
self.update(self.options.value)
}, 200)
}
GoalMeter.prototype.update = function(value) {
this.$indicatorBar.css('height', value + '%')
}
GoalMeter.DEFAULTS = {
value: 50
}
// GOALMETER PLUGIN DEFINITION
// ============================
var old = $.fn.goalMeter
$.fn.goalMeter = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.goalMeter')
var options = $.extend({}, GoalMeter.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data)
$this.data('oc.goalMeter', (data = new GoalMeter(this, options)))
else
data.update(option)
})
}
$.fn.goalMeter.Constructor = GoalMeter
// GOALMETER NO CONFLICT
// =================
$.fn.goalMeter.noConflict = function () {
$.fn.goalMeter = old
return this
}
// GOALMETER DATA-API
// ===============
$(document).render(function () {
$('[data-control=goal-meter]').goalMeter()
})
}(window.jQuery);

View File

@@ -0,0 +1,141 @@
/*
* The pie chart plugin.
*
* Data attributes:
* - data-control="chart-pie" - enables the pie chart plugin
* - data-size="200" - optional, size of the graph
* - data-center-text - text to display inside the graph
*
* JavaScript API:
* $('.scoreboard .chart').pieChart()
*
* Dependences:
* - Raphaël (raphael-min.js)
* - Winter chart utilities (chart.utils.js)
*/
+function ($) { "use strict";
var PieChart = function (element, options) {
this.options = options || {}
var
$el = this.$el = $(element),
size = this.size = (this.options.size !== undefined ? this.options.size : $el.height()),
outerRadius = size/2 - 1,
innerRadius = outerRadius - outerRadius/3.5,
values = $.wn.chartUtils.loadListValues($('ul', $el)),
$legend = $.wn.chartUtils.createLegend($('ul', $el)),
indicators = $.wn.chartUtils.initLegendColorIndicators($legend),
self = this
var $canvas = $('<div/>').addClass('canvas').width(size).height(size)
$el.prepend($canvas)
Raphael($canvas.get(0), size, size, function(){
self.paper = this
self.segments = this.set()
self.paper.customAttributes.segment = function (startAngle, endAngle) {
var
p1 = self.arcCoords(outerRadius, startAngle),
p2 = self.arcCoords(outerRadius, endAngle),
p3 = self.arcCoords(innerRadius, endAngle),
p4 = self.arcCoords(innerRadius, startAngle),
flag = (endAngle - startAngle) > 180,
path = [
["M", p1.x, p1.y],
["A", outerRadius, outerRadius, 0, +flag, 0, p2.x, p2.y],
["L", p3.x, p3.y],
["A", innerRadius, innerRadius, 0, +flag, 1, p4.x, p4.y],
["Z"]
]
return {path: path}
}
// Draw the background
self.paper.circle(size/2, size/2, innerRadius + (outerRadius - innerRadius)/2)
.attr({"stroke-width": outerRadius - innerRadius - 0.5})
.attr({stroke: $.wn.chartUtils.defaultValueColor})
// Add segments
$.each(values.values, function(index, valueInfo) {
var color = valueInfo.color !== undefined ? valueInfo.color : $.wn.chartUtils.getColor(index),
path = self.paper.path().attr({"stroke-width": 0}).attr({segment: [0, 0]}).attr({fill: color})
self.segments.push(path)
indicators[index].css('background-color', color)
path.hover(function(ev){
$.wn.chartUtils.showTooltip(ev.pageX, ev.pageY,
$.trim($.wn.chartUtils.getLegendLabel($legend, index)) + ': <strong>'+valueInfo.value+'</stong>')
}, function() {
$.wn.chartUtils.hideTooltip()
})
})
// Animate segments
var start = self.options.startAngle
$.each(values.values, function(index, valueInfo) {
var length = (values.total && valueInfo.value) ? 360/values.total * valueInfo.value : 0
if (length == 360)
length--
self.segments[index].animate({segment: [start, start + length]}, 1000, "bounce")
start += length
})
})
if (this.options.centerText !== undefined) {
var $text = $('<span>').addClass('center').html(this.options.centerText)
$canvas.append($text)
}
}
PieChart.prototype.arcCoords = function(radius, angle) {
var
a = Raphael.rad(angle),
x = this.size/2 + radius * Math.cos(a),
y = this.size/2 - radius * Math.sin(a)
return {'x': x, 'y': y}
}
PieChart.DEFAULTS = {
startAngle: 45
}
// PIECHART PLUGIN DEFINITION
// ============================
var old = $.fn.pieChart
$.fn.pieChart = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.pieChart')
var options = $.extend({}, PieChart.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data)
$this.data('oc.pieChart', new PieChart(this, options))
})
}
$.fn.pieChart.Constructor = PieChart
// PIECHART NO CONFLICT
// =================
$.fn.pieChart.noConflict = function () {
$.fn.pieChart = old
return this
}
// PIECHART DATA-API
// ===============
$(document).render(function () {
$('[data-control=chart-pie]').pieChart()
})
}(window.jQuery)

View File

@@ -0,0 +1,114 @@
/*
* Winter charting utilities.
*/
+function ($) { "use strict";
var ChartUtils = function() {}
ChartUtils.prototype.defaultValueColor = '#b8b8b8';
ChartUtils.prototype.getColor = function(index) {
var
colors = [
'#95b753', '#cc3300', '#e5a91a', '#3366ff', '#ff0f00', '#ff6600',
'#ff9e01', '#fcd202', '#f8ff01', '#b0de09', '#04d215', '#0d8ecf', '#0d52d1',
'#2a0cd0', '#8a0ccf', '#cd0d74', '#754deb', '#dddddd', '#999999', '#333333',
'#000000', '#57032a', '#ca9726', '#990000', '#4b0c25'
],
colorIndex = index % (colors.length-1);
return colors[colorIndex];
}
ChartUtils.prototype.loadListValues = function($list) {
var result = {
values: [],
total: 0,
max: 0
}
$('> li', $list).each(function(){
var value = $(this).data('value')
? parseFloat($(this).data('value'))
: parseFloat($('span', this).text());
result.total += value
result.values.push({value: value, color: $(this).data('color')})
result.max = Math.max(result.max, value)
})
return result;
}
ChartUtils.prototype.getLegendLabel = function($legend, index) {
return $('tr:eq('+index+') td:eq(1)', $legend).html();
}
ChartUtils.prototype.initLegendColorIndicators = function($legend) {
var indicators = [];
$('tr > td:first-child', $legend).each(function(){
var indicator = $('<i></i>')
$(this).prepend(indicator)
indicators.push(indicator)
})
return indicators;
}
ChartUtils.prototype.createLegend = function($list) {
var
$legend = $('<div>').addClass('chart-legend'),
$table = $('<table>')
$legend.append($table)
$('> li', $list).each(function(){
var label = $(this).clone().children().remove().end().html();
$table.append(
$('<tr>')
.append($('<td class="indicator">'))
.append($('<td>').html(label))
.append($('<td>').addClass('value').html($('span', this).html()))
)
})
$legend.insertAfter($list)
$list.remove()
return $legend;
}
ChartUtils.prototype.showTooltip = function(x, y, text) {
var $tooltip = $('#chart-tooltip')
if ($tooltip.length)
$tooltip.remove()
$tooltip = $('<div id="chart-tooltip">')
.html(text)
.css('visibility', 'hidden')
x += 10
y += 10
$(document.body).append($tooltip)
var tooltipWidth = $tooltip.outerWidth()
if ((x + tooltipWidth) > $(window).width())
x = $(window).width() - tooltipWidth - 10;
$tooltip.css({top: y, left: x, visibility: 'visible'});
}
ChartUtils.prototype.hideTooltip = function() {
$('#chart-tooltip').remove()
}
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
$.wn.chartUtils = new ChartUtils();
}(window.jQuery);

View File

@@ -0,0 +1,67 @@
/*
* Balloon selector control.
*
* Data attributes:
* - data-control="balloon-selector" - enables the plugin
*
*/
+function ($) { "use strict";
var BalloonSelector = function (element, options) {
this.$el = $(element)
this.$field = $('input', this.$el)
this.options = options || {};
var self = this;
$('li', this.$el).click(function(){
if (self.$el.hasClass('control-disabled')) {
return
}
$('li', self.$el).removeClass('active')
$(this).addClass('active')
self.$field
.val($(this).data('value'))
.trigger('change')
})
}
BalloonSelector.DEFAULTS = {}
// BALLOON SELECTOR PLUGIN DEFINITION
// ===================================
var old = $.fn.balloonSelector
$.fn.balloonSelector = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.balloon-selector')
var options = $.extend({}, BalloonSelector.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.balloon-selector', (data = new BalloonSelector(this, options)))
})
}
$.fn.balloonSelector.Constructor = BalloonSelector
// BALLOON SELECTOR NO CONFLICT
// ===================================
$.fn.balloonSelector.noConflict = function () {
$.fn.balloonSelector = old
return this
}
// BALLOON SELECTOR DATA-API
// ===================================
$(document).on('render', function(){
$('div[data-control=balloon-selector]').balloonSelector()
})
}(window.jQuery);

View File

@@ -0,0 +1,86 @@
/*
* Checkbox control
*
*/
(function($) {
$(document).on('keypress', 'div.custom-checkbox', function(e) {
if (e.key === '(Space character)' || e.key === 'Spacebar' || e.key === ' ') {
var $cb = $('input[type=checkbox]', this)
if ($cb.data('wn-space-timestamp') == e.timeStamp)
return
$cb.get(0).checked = !$cb.get(0).checked
$cb.data('wn-space-timestamp', e.timeStamp)
$cb.trigger('change')
return false
}
})
//
// Intermediate checkboxes
//
$(document).render(function() {
$('div.custom-checkbox.is-indeterminate > input').each(function() {
var $el = $(this),
checked = $el.data('checked')
switch (checked) {
// Unchecked
case 1:
$el.prop('indeterminate', true)
break
// Checked
case 2:
$el.prop('indeterminate', false)
$el.prop('checked', true)
break
// Unchecked
default:
$el.prop('indeterminate', false)
$el.prop('checked', false)
}
})
})
$(document).on('click', 'div.custom-checkbox.is-indeterminate > label', function() {
var $el = $(this).parent().find('input:first'),
checked = $el.data('checked')
if (checked === undefined) {
checked = $el.is(':checked') ? 1 : 0
}
switch (checked) {
// Unchecked, going indeterminate
case 0:
$el.data('checked', 1)
$el.prop('indeterminate', true)
break
// Indeterminate, going checked
case 1:
$el.data('checked', 2)
$el.prop('indeterminate', false)
$el.prop('checked', true)
break
// Checked, going unchecked
default:
$el.data('checked', 0)
$el.prop('indeterminate', false)
$el.prop('checked', false)
}
$el.trigger('change')
return false
})
})(jQuery);

View File

@@ -0,0 +1,369 @@
/*
* DatePicker plugin
*
* - Documentation: ../docs/datepicker.md
*
* Dependences:
* - Pikaday plugin (pikaday.js)
* - Pikaday jQuery addon (pikaday.jquery.js)
* - Clockpicker plugin (jquery-clockpicker.js)
* - Moment library (moment.js)
* - Moment Timezone library (moment.timezone.js)
*/
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var DatePicker = function (element, options) {
this.$el = $(element)
this.options = options || {}
$.wn.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
DatePicker.prototype = Object.create(BaseProto)
DatePicker.prototype.constructor = DatePicker
DatePicker.prototype.init = function() {
var self = this,
$form = this.$el.closest('form'),
changeMonitor = $form.data('oc.changeMonitor')
if (changeMonitor !== undefined) {
changeMonitor.pause()
}
this.dbDateTimeFormat = 'YYYY-MM-DD HH:mm:ss'
this.dbDateFormat = 'YYYY-MM-DD'
this.dbTimeFormat = 'HH:mm:ss'
this.$dataLocker = $('[data-datetime-value]', this.$el)
this.$datePicker = $('[data-datepicker]', this.$el)
this.$timePicker = $('[data-timepicker]', this.$el)
this.hasDate = !!this.$datePicker.length
this.hasTime = !!this.$timePicker.length
this.ignoreTimezone = this.$el.get(0).hasAttribute('data-ignore-timezone')
this.initRegion()
if (this.hasDate) {
this.initDatePicker()
}
if (this.hasTime) {
this.initTimePicker()
}
if (changeMonitor !== undefined) {
changeMonitor.resume()
}
this.$timePicker.on('change.oc.datepicker', function() {
if (!$.trim($(this).val())) {
self.emptyValues()
}
else {
self.onSelectTimePicker()
}
})
this.$datePicker.on('change.oc.datepicker', function() {
if (!$.trim($(this).val())) {
self.emptyValues()
}
})
this.$el.one('dispose-control', this.proxy(this.dispose))
}
DatePicker.prototype.dispose = function() {
this.$timePicker.off('change.oc.datepicker')
this.$datePicker.off('change.oc.datepicker')
this.$el.off('dispose-control', this.proxy(this.dispose))
this.$el.removeData('oc.datePicker')
this.$el = null
this.options = null
BaseProto.dispose.call(this)
}
//
// Datepicker
//
DatePicker.prototype.initDatePicker = function() {
var self = this,
dateFormat = this.getDateFormat(),
now = moment().tz(this.timezone).format(dateFormat)
var pikadayOptions = {
yearRange: this.options.yearRange,
firstDay: this.options.firstDay,
showWeekNumber: this.options.showWeekNumber,
format: dateFormat,
setDefaultDate: now,
onOpen: function() {
var $field = $(this._o.trigger)
$(this.el).css({
left: 'auto',
right: $(window).width() - $field.offset().left - $field.outerWidth()
})
},
onSelect: function() {
self.onSelectDatePicker.call(self, this.getMoment())
}
}
var lang = this.getLang('datepicker', false)
if (lang) {
pikadayOptions.i18n = lang
}
this.$datePicker.val(this.getDataLockerValue(dateFormat))
if (this.options.minDate) {
pikadayOptions.minDate = new Date(this.options.minDate)
}
if (this.options.maxDate) {
pikadayOptions.maxDate = new Date(this.options.maxDate)
}
this.$datePicker.pikaday(pikadayOptions)
// Avoid displaying keyboards on mobile when the widget is displayed
if (!this.$datePicker.attr('inputmode')) {
this.$datePicker.attr('inputmode', 'none')
}
}
DatePicker.prototype.onSelectDatePicker = function(pickerMoment) {
var pickerValue = pickerMoment.format(this.dbDateFormat)
var timeValue = this.options.mode === 'date' ? '00:00:00' : this.getTimePickerValue()
var momentObj = moment
.tz(pickerValue + ' ' + timeValue, this.dbDateTimeFormat, this.timezone)
.tz(this.appTimezone)
var lockerValue = momentObj.format(this.dbDateTimeFormat)
this.$dataLocker.val(lockerValue)
}
// Returns in user preference timezone
DatePicker.prototype.getDatePickerValue = function() {
var value = this.$datePicker.val()
if (!this.hasDate || !value) {
return moment.tz(this.appTimezone)
.tz(this.timezone)
.format(this.dbDateFormat)
}
return moment(value, this.getDateFormat()).format(this.dbDateFormat)
}
DatePicker.prototype.getDateFormat = function() {
var format = 'YYYY-MM-DD'
if (this.options.format) {
format = this.options.format
}
else if (this.locale) {
format = moment()
.locale(this.locale)
.localeData()
.longDateFormat('l')
}
return format
}
//
// Timepicker
//
DatePicker.prototype.initTimePicker = function() {
this.$timePicker.clockpicker({
autoclose: 'true',
placement: 'auto',
align: 'right',
twelvehour: this.isTimeTwelveHour(),
afterDone: this.proxy(this.onChangeTimePicker)
})
this.$timePicker.val(this.getDataLockerValue(this.getTimeFormat()))
// Avoid displaying keyboards on mobile when the widget is displayed
if (!this.$timePicker.attr('inputmode')) {
this.$timePicker.attr('inputmode', 'none')
}
}
DatePicker.prototype.onSelectTimePicker = function() {
var pickerValue = this.$timePicker.val()
var timeValue = moment(pickerValue, this.getTimeFormat()).format(this.dbTimeFormat)
var dateValue = this.getDatePickerValue()
var momentObj = moment
.tz(dateValue + ' ' + timeValue, this.dbDateTimeFormat, this.timezone)
.tz(this.appTimezone)
var lockerValue = momentObj.format(this.dbDateTimeFormat)
this.$dataLocker.val(lockerValue)
}
DatePicker.prototype.onChangeTimePicker = function() {
// Trigger a change event when the time is changed, to allow dependent fields to refresh
this.$timePicker.trigger('change')
}
// Returns in user preference timezone
DatePicker.prototype.getTimePickerValue = function() {
var value = this.$timePicker.val()
if (!this.hasTime || !value) {
return moment.tz(this.appTimezone)
.tz(this.timezone)
.format(this.dbTimeFormat)
}
return moment(value, this.getTimeFormat()).format(this.dbTimeFormat)
}
DatePicker.prototype.getTimeFormat = function() {
return this.isTimeTwelveHour()
? 'hh:mm A'
: 'HH:mm'
}
DatePicker.prototype.isTimeTwelveHour = function() {
return false
// Disabled for now: The analog clock design is pretty good
// at representing time regardless of the format. If we want
// to enable this, there should be some way to disable it
// again via the form field options.
// var momentObj = moment()
// if (this.locale) {
// momentObj = momentObj.locale(this.locale)
// }
// return momentObj
// .localeData()
// .longDateFormat('LT')
// .indexOf('A') !== -1;
}
//
// Utilities
//
DatePicker.prototype.emptyValues = function() {
this.$dataLocker.val('')
this.$datePicker.val('')
this.$timePicker.val('')
}
DatePicker.prototype.getDataLockerValue = function(format) {
var value = this.$dataLocker.val()
return value
? this.getMomentLoadValue(value, format)
: null
}
DatePicker.prototype.getMomentLoadValue = function(value, format) {
var momentObj = moment.tz(value, this.appTimezone)
if (this.locale) {
momentObj = momentObj.locale(this.locale)
}
momentObj = momentObj.tz(this.timezone)
return momentObj.format(format)
}
DatePicker.prototype.initRegion = function() {
this.locale = $('meta[name="backend-locale"]').attr('content')
this.timezone = $('meta[name="backend-timezone"]').attr('content')
this.appTimezone = $('meta[name="app-timezone"]').attr('content')
if (!this.appTimezone) {
this.appTimezone = 'UTC'
}
if (!this.timezone) {
this.timezone = 'UTC'
}
// Set both timezones to UTC to disable converting between them
if (this.ignoreTimezone) {
this.appTimezone = 'UTC'
this.timezone = 'UTC'
}
}
DatePicker.prototype.getLang = function(name, defaultValue) {
if ($.oc === undefined || $.wn.lang === undefined) {
return defaultValue
}
return $.wn.lang.get(name, defaultValue)
}
DatePicker.DEFAULTS = {
minDate: null,
maxDate: null,
format: null,
yearRange: 10,
firstDay: 0,
showWeekNumber: false,
mode: 'datetime'
}
// PLUGIN DEFINITION
// ============================
var old = $.fn.datePicker
$.fn.datePicker = function (option) {
var args = Array.prototype.slice.call(arguments, 1), items, result
items = this.each(function () {
var $this = $(this)
var data = $this.data('oc.datePicker')
var options = $.extend({}, DatePicker.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.datePicker', (data = new DatePicker(this, options)))
if (typeof option == 'string') result = data[option].apply(data, args)
if (typeof result != 'undefined') return false
})
return result ? result : items
}
$.fn.datePicker.Constructor = DatePicker
$.fn.datePicker.noConflict = function () {
$.fn.datePicker = old
return this
}
$(document).on('render', function() {
$('[data-control="datepicker"]').datePicker()
});
}(window.jQuery);

View File

@@ -0,0 +1,445 @@
/*
* Allows to scroll an element content in the horizontal or horizontal directions. This script doesn't use
* absolute positioning and rely on the scrollLeft/scrollTop DHTML properties. The element width should be
* fixed with the CSS or JavaScript.
*
* Events triggered on the element:
* - start.oc.dragScroll
* - drag.oc.dragScroll
* - stop.oc.dragScroll
*
* Options:
* - start - callback function to execute when the drag starts
* - drag - callback function to execute when the element is dragged
* - stop - callback function to execute when the drag ends
* - vertical - determines if the scroll direction is vertical, true by default
* - scrollClassContainer - if specified, specifies an element or element selector to apply the 'scroll-before' and 'scroll-after' CSS classes,
* depending on whether the scrollable area is in its start or end
* - scrollMarkerContainer - if specified, specifies an element or element selector to inject scroll markers (span elements that con
* contain the ellipses icon, indicating whether scrolling is possible)
* - useDrag - determines if dragging is allowed support, true by default
* - useNative - if native CSS is enabled via "mobile" on the HTML tag, false by default
* - useScroll - determines if the mouse wheel scrolling is allowed, true by default
* - useComboScroll - determines if horizontal scroll should act as vertical, and vice versa, true by default
* - dragSelector - restrict drag events to this selector
* - scrollSelector - restrict scroll events to this selector
*
* Methods:
* - isStart - determines if the scrollable area is in its start (left or top)
* - isEnd - determines if the scrollable area is in its end (right or bottom)
* - goToStart - moves the scrollable area to the start (left or top)
* - goToElement - moves the scrollable area to an element
*
* Require:
* - modernizr/modernizr
* - mousewheel/mousewheel
*/
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var DragScroll = function (element, options) {
this.options = $.extend({}, DragScroll.DEFAULTS, options)
var
$el = $(element),
el = $el.get(0),
dragStart = 0,
startOffset = 0,
self = this,
dragging = false,
eventElementName = this.options.vertical ? 'pageY' : 'pageX',
isNative = this.options.useNative && $('html').hasClass('mobile');
this.el = $el
this.scrollClassContainer = this.options.scrollClassContainer ? $(this.options.scrollClassContainer) : $el
this.isScrollable = true
Base.call(this)
/*
* Inject scroll markers
*/
if (this.options.scrollMarkerContainer) {
$(this.options.scrollMarkerContainer)
.append($('<span class="before scroll-marker"></span><span class="after scroll-marker"></span>'))
}
/*
* Bind events
*/
var $scrollSelect = this.options.scrollSelector ? $(this.options.scrollSelector, $el) : $el
$scrollSelect.mousewheel(function(event){
if (!self.options.useScroll) {
return;
}
var offset,
offsetX = event.deltaFactor * event.deltaX,
offsetY = event.deltaFactor * event.deltaY
if (!offsetX && self.options.useComboScroll) {
offset = offsetY * -1
}
else if (!offsetY && self.options.useComboScroll) {
offset = offsetX
}
else {
offset = self.options.vertical ? (offsetY * -1) : offsetX
}
return !scrollWheel(offset)
})
if (this.options.useDrag) {
$el.on('mousedown.dragScroll', this.options.dragSelector, function(event){
if (event.target && event.target.tagName === 'INPUT') {
return // Don't prevent clicking inputs in the toolbar
}
if (!self.isScrollable) {
return
}
startDrag(event)
return false
})
}
if (Modernizr.touchevents) {
$el.on('touchstart.dragScroll', this.options.dragSelector, function(event){
var touchEvent = event.originalEvent
if (touchEvent.touches.length == 1) {
startDrag(touchEvent.touches[0])
event.stopPropagation()
}
})
}
$el.on('click.dragScroll', function() {
// Do not handle item clicks while dragging
if ($(document.body).hasClass(self.options.dragClass)) {
return false
}
})
$(document).on('ready', this.proxy(this.fixScrollClasses))
$(window).on('resize', this.proxy(this.fixScrollClasses))
/*
* Internal event, drag has started
*/
function startDrag(event) {
dragStart = event[eventElementName]
startOffset = self.options.vertical ? $el.scrollTop() : $el.scrollLeft()
if (Modernizr.touchevents) {
$(window).on('touchmove.dragScroll', function(event) {
var touchEvent = event.originalEvent
moveDrag(touchEvent.touches[0])
if (!isNative) {
event.preventDefault()
}
})
$(window).on('touchend.dragScroll', function(event) {
stopDrag()
})
}
$(window).on('mousemove.dragScroll', function(event) {
moveDrag(event)
return false
})
$(window).on('mouseup.dragScroll', function(mouseUpEvent) {
var isClick = event.pageX == mouseUpEvent.pageX && event.pageY == mouseUpEvent.pageY
stopDrag(isClick)
return false
})
}
/*
* Internal event, drag is active
*/
function moveDrag(event) {
var current = event[eventElementName],
offset = dragStart - current
if (Math.abs(offset) > 3) {
if (!dragging) {
dragging = true
$el.trigger('start.oc.dragScroll')
self.options.start()
$(document.body).addClass(self.options.dragClass)
}
if (!isNative) {
self.options.vertical
? $el.scrollTop(startOffset + offset)
: $el.scrollLeft(startOffset + offset)
}
$el.trigger('drag.oc.dragScroll')
self.options.drag()
}
}
/*
* Internal event, drag has ended
*/
function stopDrag(click) {
$(window).off('.dragScroll')
dragging = false;
if (click) {
$(document.body).removeClass(self.options.dragClass)
}
else {
self.fixScrollClasses()
}
window.setTimeout(function(){
if (!click) {
$(document.body).removeClass(self.options.dragClass)
$el.trigger('stop.oc.dragScroll')
self.options.stop()
self.fixScrollClasses()
}
}, 100)
}
/*
* Scroll wheel has moved by supplied offset
*/
function scrollWheel(offset) {
startOffset = self.options.vertical ? el.scrollTop : el.scrollLeft
self.options.vertical
? $el.scrollTop(startOffset + offset)
: $el.scrollLeft(startOffset + offset)
var scrolled = self.options.vertical
? el.scrollTop != startOffset
: el.scrollLeft != startOffset
$el.trigger('drag.oc.dragScroll')
self.options.drag()
if (scrolled) {
if (self.wheelUpdateTimer !== undefined && self.wheelUpdateTimer !== false)
window.clearInterval(self.wheelUpdateTimer);
self.wheelUpdateTimer = window.setTimeout(function() {
self.wheelUpdateTimer = false;
self.fixScrollClasses()
}, 100);
}
return scrolled
}
this.fixScrollClasses();
}
DragScroll.prototype = Object.create(BaseProto)
DragScroll.prototype.constructor = DragScroll
DragScroll.DEFAULTS = {
vertical: false,
useDrag: true,
useScroll: true,
useNative: false,
useComboScroll: true,
scrollClassContainer: false,
scrollMarkerContainer: false,
scrollSelector: null,
dragSelector: null,
dragClass: 'drag',
start: function() {},
drag: function() {},
stop: function() {}
}
DragScroll.prototype.fixScrollClasses = function() {
var isStart = this.isStart(),
isEnd = this.isEnd()
this.scrollClassContainer.toggleClass('scroll-before', !isStart)
this.scrollClassContainer.toggleClass('scroll-after', !isEnd)
this.scrollClassContainer.toggleClass('scroll-active-before', this.isActiveBefore())
this.scrollClassContainer.toggleClass('scroll-active-after', this.isActiveAfter())
this.isScrollable = !isStart || !isEnd
}
DragScroll.prototype.isStart = function() {
if (!this.options.vertical) {
return this.el.scrollLeft() <= 0;
}
else {
return this.el.scrollTop() <= 0;
}
}
DragScroll.prototype.isEnd = function() {
if (!this.options.vertical) {
return (this.el[0].scrollWidth - (this.el.scrollLeft() + this.el.width())) <= 0
}
else {
return (this.el[0].scrollHeight - (this.el.scrollTop() + this.el.height())) <= 0
}
}
DragScroll.prototype.goToStart = function() {
if (!this.options.vertical) {
return this.el.scrollLeft(0)
}
else {
return this.el.scrollTop(0)
}
}
/*
* Determines if the element with the class 'active' is hidden before the viewport -
* on the left or on the top, depending on whether the scrollbar is horizontal or vertical.
*/
DragScroll.prototype.isActiveAfter = function() {
var activeElement = $('.active', this.el);
if (activeElement.length == 0) {
return false
}
if (!this.options.vertical) {
return activeElement.get(0).offsetLeft > (this.el.scrollLeft() + this.el.width())
}
else {
return activeElement.get(0).offsetTop > (this.el.scrollTop() + this.el.height())
}
}
/*
* Determines if the element with the class 'active' is hidden after the viewport -
* on the right or on the bottom, depending on whether the scrollbar is horizontal or vertical.
*/
DragScroll.prototype.isActiveBefore = function() {
var activeElement = $('.active', this.el);
if (activeElement.length == 0) {
return false
}
if (!this.options.vertical) {
return (activeElement.get(0).offsetLeft + activeElement.width()) < this.el.scrollLeft()
}
else {
return (activeElement.get(0).offsetTop + activeElement.height()) < this.el.scrollTop()
}
}
DragScroll.prototype.goToElement = function(element, callback, options) {
var $el = $(element)
if (!$el.length)
return;
var self = this,
params = {
duration: 300,
queue: false,
complete: function(){
self.fixScrollClasses()
if (callback !== undefined)
callback()
}
}
params = $.extend(params, options || {})
var offset = 0,
animated = false
if (!this.options.vertical) {
offset = $el.get(0).offsetLeft - this.el.scrollLeft()
if (offset < 0) {
this.el.animate({'scrollLeft': $el.get(0).offsetLeft}, params)
animated = true
}
else {
offset = $el.get(0).offsetLeft + $el.width() - (this.el.scrollLeft() + this.el.width())
if (offset > 0) {
this.el.animate({'scrollLeft': $el.get(0).offsetLeft + $el.width() - this.el.width()}, params)
animated = true
}
}
}
else {
offset = $el.get(0).offsetTop - this.el.scrollTop()
if (offset < 0) {
this.el.animate({'scrollTop': $el.get(0).offsetTop}, params)
animated = true
}
else {
offset = $el.get(0).offsetTop - (this.el.scrollTop() + this.el.height())
if (offset > 0) {
this.el.animate({'scrollTop': $el.get(0).offsetTop + $el.height() - this.el.height()}, params)
animated = true
}
}
}
if (!animated && callback !== undefined) {
callback()
}
}
DragScroll.prototype.dispose = function() {
this.scrollClassContainer = null
$(document).off('ready', this.proxy(this.fixScrollClasses))
$(window).off('resize', this.proxy(this.fixScrollClasses))
this.el.off('.dragScroll')
this.el.removeData('oc.dragScroll')
this.el = null
BaseProto.dispose.call(this)
}
// DRAGSCROLL PLUGIN DEFINITION
// ============================
var old = $.fn.dragScroll
$.fn.dragScroll = function (option) {
var args = arguments;
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.dragScroll')
var options = typeof option == 'object' && option
if (!data) $this.data('oc.dragScroll', (data = new DragScroll(this, options)))
if (typeof option == 'string') {
var methodArgs = [];
for (var i=1; i<args.length; i++)
methodArgs.push(args[i])
data[option].apply(data, methodArgs)
}
})
}
$.fn.dragScroll.Constructor = DragScroll
// DRAGSCROLL NO CONFLICT
// =================
$.fn.dragScroll.noConflict = function () {
$.fn.dragScroll = old
return this
}
}(window.jQuery);

View File

@@ -0,0 +1,217 @@
/*
* Sortable plugin.
*
* Documentation: ../docs/drag-sort.md
*
* Require:
* - sortable/jquery-sortable
*/
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var Sortable = function (element, options) {
this.$el = $(element)
this.options = options || {}
this.cursorAdjustment = null
$.wn.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
Sortable.prototype = Object.create(BaseProto)
Sortable.prototype.constructor = Sortable
Sortable.prototype.init = function() {
this.$el.one('dispose-control', this.proxy(this.dispose))
var
self = this,
sortableOverrides = {},
sortableDefaults = {
onDragStart: this.proxy(this.onDragStart),
onDrag: this.proxy(this.onDrag),
onDrop: this.proxy(this.onDrop)
}
/*
* Override _super object for each option/event
*/
if (this.options.onDragStart) {
sortableOverrides.onDragStart = function ($item, container, _super, event) {
self.options.onDragStart($item, container, sortableDefaults.onDragStart, event)
}
}
if (this.options.onDrag) {
sortableOverrides.onDrag = function ($item, position, _super, event) {
self.options.onDrag($item, position, sortableDefaults.onDrag, event)
}
}
if (this.options.onDrop) {
sortableOverrides.onDrop = function ($item, container, _super, event) {
self.options.onDrop($item, container, sortableDefaults.onDrop, event)
}
}
this.$el.jqSortable($.extend({}, sortableDefaults, this.options, sortableOverrides))
}
Sortable.prototype.dispose = function() {
this.$el.jqSortable('destroy')
this.$el.off('dispose-control', this.proxy(this.dispose))
this.$el.removeData('oc.sortable')
this.$el = null
this.options = null
this.cursorAdjustment = null
BaseProto.dispose.call(this)
}
Sortable.prototype.onDragStart = function ($item, container, _super, event) {
/*
* Relative cursor position
*/
var offset = $item.offset(),
pointer = container.rootGroup.pointer
if (pointer) {
this.cursorAdjustment = {
left: pointer.left - offset.left,
top: pointer.top - offset.top
}
}
else {
this.cursorAdjustment = null
}
if (this.options.tweakCursorAdjustment) {
this.cursorAdjustment = this.options.tweakCursorAdjustment(this.cursorAdjustment)
}
$item.css({
height: $item.height(),
width: $item.width()
})
$item.addClass('dragged')
$('body').addClass('dragging')
this.$el.addClass('dragging')
/*
* Use animation
*/
if (this.options.useAnimation) {
$item.data('oc.animated', true)
}
/*
* Placeholder clone
*/
if (this.options.usePlaceholderClone) {
$(container.rootGroup.placeholder).html($item.html())
}
if (!this.options.useDraggingClone) {
$item.hide()
}
}
Sortable.prototype.onDrag = function ($item, position, _super, event) {
if (this.cursorAdjustment) {
/*
* Relative cursor position
*/
$item.css({
left: position.left - this.cursorAdjustment.left,
top: position.top - this.cursorAdjustment.top
})
}
else {
/*
* Default behavior
*/
$item.css(position)
}
}
Sortable.prototype.onDrop = function ($item, container, _super, event) {
$item.removeClass('dragged').removeAttr('style')
$('body').removeClass('dragging')
this.$el.removeClass('dragging')
if ($item.data('oc.animated')) {
$item
.hide()
.slideDown(200)
}
}
//
// Proxy API
//
Sortable.prototype.enable = function() {
this.$el.jqSortable('enable')
}
Sortable.prototype.disable = function() {
this.$el.jqSortable('disable')
}
Sortable.prototype.refresh = function() {
this.$el.jqSortable('refresh')
}
Sortable.prototype.serialize = function() {
this.$el.jqSortable('serialize')
}
Sortable.prototype.destroy = function() {
this.dispose()
}
// External solution for group persistence
// See https://github.com/johnny/jquery-sortable/pull/122
Sortable.prototype.destroyGroup = function() {
var jqSortable = this.$el.data('jqSortable')
if (jqSortable.group) {
jqSortable.group._destroy()
}
}
Sortable.DEFAULTS = {
useAnimation: false,
usePlaceholderClone: false,
useDraggingClone: true,
tweakCursorAdjustment: null
}
// PLUGIN DEFINITION
// ============================
var old = $.fn.sortable
$.fn.sortable = function (option) {
var args = arguments;
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.sortable')
var options = $.extend({}, Sortable.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.sortable', (data = new Sortable(this, options)))
if (typeof option == 'string') data[option].apply(data, args)
})
}
$.fn.sortable.Constructor = Sortable
$.fn.sortable.noConflict = function () {
$.fn.sortable = old
return this
}
}(window.jQuery);

View File

@@ -0,0 +1,192 @@
/*
* Drag Value plugin
*
* Uses native dragging to allow elements to be dragged in to inputs, textareas, etc
*
* Data attributes:
* - data-control="dragvalue" - enables the plugin on an element
* - data-text-value="text to include" - text value to include when dragging
* - data-drag-click="false" - allow click event, tries to cache the last active element
* and insert the text at the current cursor position
*
* JavaScript API:
* $('a#someElement').dragValue({ textValue: 'insert this text' })
*
*/
+function ($) { "use strict";
// DRAG VALUE CLASS DEFINITION
// ============================
var DragValue = function(element, options) {
this.options = options
this.$el = $(element)
// Init
this.init()
}
DragValue.DEFAULTS = {
dragClick: false
}
DragValue.prototype.init = function() {
this.$el.prop('draggable', true)
this.textValue = this.$el.data('textValue')
this.$el.on('dragstart', $.proxy(this.handleDragStart, this))
this.$el.on('drop', $.proxy(this.handleDrop, this))
this.$el.on('dragend', $.proxy(this.handleDragEnd, this))
if (this.options.dragClick) {
this.$el.on('click', $.proxy(this.handleClick, this))
this.$el.on('mouseover', $.proxy(this.handleMouseOver, this))
}
}
//
// Drag events
//
DragValue.prototype.handleDragStart = function(event) {
var e = event.originalEvent
e.dataTransfer.effectAllowed = 'all'
e.dataTransfer.setData('text/plain', this.textValue)
this.$el
.css({ opacity: 0.5 })
.addClass('dragvalue-dragging')
}
DragValue.prototype.handleDrop = function(event) {
event.stopPropagation()
return false
}
DragValue.prototype.handleDragEnd = function(event) {
this.$el
.css({ opacity: 1 })
.removeClass('dragvalue-dragging')
}
//
// Click events
//
DragValue.prototype.handleMouseOver = function(event) {
var el = document.activeElement
if (!el) return
if (el.isContentEditable || (
el.tagName.toLowerCase() == 'input' &&
el.type == 'text' ||
el.tagName.toLowerCase() == 'textarea'
)) {
this.lastElement = el
}
}
DragValue.prototype.handleClick = function(event) {
if (!this.lastElement) return
var $el = $(this.lastElement)
if ($el.closest('[data-control=codeeditor]').length)
return this.handleClickCodeEditor(event, $el)
if (this.lastElement.isContentEditable)
return this.handleClickContentEditable()
this.insertAtCaret(this.lastElement, this.textValue)
}
DragValue.prototype.handleClickCodeEditor = function(event, $el) {
var $editorArea = $el.closest('[data-control=codeeditor]')
if (!$editorArea.length) return
var wrapper = $editorArea.data('oc.codeEditor')
if (wrapper && wrapper.insert) {
wrapper.insert(this.textValue)
}
}
DragValue.prototype.handleClickContentEditable = function() {
var sel, range, html;
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode( document.createTextNode(this.textValue) );
}
}
else if (document.selection && document.selection.createRange) {
document.selection.createRange().text = this.textValue;
}
}
//
// Helpers
//
DragValue.prototype.insertAtCaret = function(el, insertValue) {
// IE
if (document.selection) {
el.focus()
var sel = document.selection.createRange()
sel.text = insertValue
el.focus()
}
// Real browsers
else if (el.selectionStart || el.selectionStart == '0') {
var startPos = el.selectionStart, endPos = el.selectionEnd, scrollTop = el.scrollTop
el.value = el.value.substring(0, startPos) + insertValue + el.value.substring(endPos, el.value.length)
el.focus()
el.selectionStart = startPos + insertValue.length
el.selectionEnd = startPos + insertValue.length
el.scrollTop = scrollTop
}
else {
el.value += insertValue
el.focus()
}
}
// DRAG VALUE PLUGIN DEFINITION
// ============================
var old = $.fn.dragValue
$.fn.dragValue = function (option) {
var args = Array.prototype.slice.call(arguments, 1), result
this.each(function () {
var $this = $(this)
var data = $this.data('oc.dragvalue')
var options = $.extend({}, DragValue.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.dragvalue', (data = new DragValue(this, options)))
if (typeof option == 'string') result = data[option].apply(data, args)
if (typeof result != 'undefined') return false
})
return result ? result : this
}
$.fn.dragValue.Constructor = DragValue
// DRAG VALUE NO CONFLICT
// =================
$.fn.dragValue.noConflict = function () {
$.fn.dragValue = old
return this
}
// DRAG VALUE DATA-API
// ===============
$(document).render(function() {
$('[data-control="dragvalue"]').dragValue()
});
}(window.jQuery);

View File

@@ -0,0 +1,144 @@
/*
* Dropdown menus.
*
* This script customizes the Twitter Bootstrap drop-downs.
*
* Require:
* - bootstrap/dropdown
*/
+function ($) { "use strict";
$(document).on('shown.bs.dropdown', '.dropdown', function(event, relatedTarget) {
$(document.body).addClass('dropdown-open')
var dropdown = $(relatedTarget.relatedTarget).siblings('.dropdown-menu'),
dropdownContainer = $(this).data('dropdown-container')
// The dropdown menu should be a sibling of the triggering element (above)
// otherwise, look for any dropdown menu within this context.
if (dropdown.length === 0){
dropdown = $('.dropdown-menu', this)
}
if ($('.dropdown-container', dropdown).length == 0) {
var title = $('[data-toggle=dropdown]', this).text(),
titleAttr = dropdown.data('dropdown-title'),
timer = null
if (titleAttr !== undefined)
title = titleAttr
$('li:first-child', dropdown).addClass('first-item')
$('li:last-child', dropdown).addClass('last-item')
dropdown.prepend($('<li />').addClass('dropdown-title').text(title))
var container = $('<li />').addClass('dropdown-container'),
ul = $('<ul />')
container.prepend(ul)
ul.prepend(dropdown.children())
dropdown.prepend(container)
dropdown.on('touchstart', function(){
window.setTimeout(function(){
dropdown.addClass('scroll')
}, 200)
})
dropdown.on('touchend', function(){
window.setTimeout(function(){
dropdown.removeClass('scroll')
}, 200)
})
dropdown.on('click', 'a', function(){
if (dropdown.hasClass('scroll'))
return false
})
}
if (dropdownContainer !== undefined && dropdownContainer == 'body') {
$(this).data('oc.dropdown', dropdown)
$(document.body).append(dropdown)
dropdown.css({
'visibility': 'hidden',
'left': 0,
'top' : 0,
'display': 'block'
})
var targetOffset = $(this).offset(),
targetHeight = $(this).height(),
targetWidth = $(this).width(),
position = {
x: targetOffset.left,
y: targetOffset.top + targetHeight
},
leftOffset = targetWidth < 30 ? -16 : 0,
documentHeight = $(document).height(),
dropdownHeight = dropdown.height()
if ((dropdownHeight + position.y) > $(document).height()) {
position.y = targetOffset.top - dropdownHeight - 12
dropdown.addClass('top')
}
else {
dropdown.removeClass('top')
}
dropdown.css({
'left': position.x + leftOffset,
'top': position.y,
'visibility': 'visible'
})
}
if ($('.dropdown-overlay', document.body).length == 0) {
$(document.body).prepend($('<div/>').addClass('dropdown-overlay'));
}
})
$(document).on('hidden.bs.dropdown', '.dropdown', function() {
var dropdown = $(this).data('oc.dropdown')
if (dropdown !== undefined) {
dropdown.css('display', 'none')
$(this).append(dropdown)
}
$(document.body).removeClass('dropdown-open');
})
/*
* Fixed positioned dropdowns
* - Useful for dropdowns inside hidden overflow containers
*/
var $dropdown, $container, $target
function fixDropdownPosition() {
var position = $container.offset()
$dropdown.css({
position: 'fixed',
top: position.top - 1 - $(window).scrollTop() + $target.outerHeight(),
left: position.left
})
}
$(document).on('shown.bs.dropdown', '.dropdown.dropdown-fixed', function(event, eventData) {
$container = $(this)
$dropdown = $('.dropdown-menu', $container)
$target = $(eventData.relatedTarget)
fixDropdownPosition()
$(window).on('scroll.oc.dropdown, resize.oc.dropdown', fixDropdownPosition)
})
$(document).on('hidden.bs.dropdown', '.dropdown.dropdown-fixed', function() {
$(window).off('scroll.oc.dropdown, resize.oc.dropdown', fixDropdownPosition)
})
}(window.jQuery);

View File

@@ -0,0 +1,414 @@
/*
* Filter Widget
*
* Data attributes:
* - data-behavior="filter" - enables the filter plugin
*
* Dependences:
* - Winter Popover (winter.popover.js)
*
* Notes:
* Ideally this control would not depend on loader or the AJAX framework,
* then the Filter widget can use events to handle this business logic.
*
* Require:
* - mustache/mustache
* - modernizr/modernizr
* - storm/popover
*/
+function ($) {
"use strict";
var FilterWidget = $.fn.filterWidget.Constructor;
// OVERLOADED MODULE
// =================
var overloaded_init = FilterWidget.prototype.init;
FilterWidget.prototype.init = function () {
overloaded_init.apply(this)
var self = this;
this.$el.children().each(function(key, $filter) {
if ($filter.hasAttribute('data-ignore-timezone')) {
self.ignoreTimezone = true;
}
});
this.initRegion()
this.initFilterDate()
}
// NEW MODULE
// =================
FilterWidget.prototype.initFilterDate = function () {
var self = this
this.$el.on('show.oc.popover', 'a.filter-scope-date', function (event) {
self.initDatePickers($(this).hasClass('range'))
$(event.relatedTarget).on('click', '#controlFilterPopoverDate [data-filter-action="filter"]', function (e) {
e.preventDefault()
e.stopPropagation()
self.filterByDate()
})
$(event.relatedTarget).on('click', '#controlFilterPopoverDate [data-filter-action="clear"]', function (e) {
e.preventDefault()
e.stopPropagation()
self.filterByDate(true)
})
})
this.$el.on('hiding.oc.popover', 'a.filter-scope-date', function () {
self.clearDatePickers()
})
this.$el.on('hide.oc.popover', 'a.filter-scope-date', function () {
var $scope = $(this)
self.pushOptions(self.activeScopeName)
self.activeScopeName = null
self.$activeScope = null
// Second click closes the filter scope
setTimeout(function () {
$scope.removeClass('filter-scope-open')
}, 200)
})
this.$el.on('click', 'a.filter-scope-date', function () {
var $scope = $(this),
scopeName = $scope.data('scope-name')
// Ignore if already opened
if ($scope.hasClass('filter-scope-open')) return
// Ignore if another popover is opened
if (null !== self.activeScopeName) return
self.$activeScope = $scope
self.activeScopeName = scopeName
self.isActiveScopeDirty = false
if ($scope.hasClass('range')) {
self.displayPopoverRange($scope)
}
else {
self.displayPopoverDate($scope)
}
$scope.addClass('filter-scope-open')
})
}
/*
* Get popover date template
*/
FilterWidget.prototype.getPopoverDateTemplate = function () {
return ' \
<form id="controlFilterPopoverDate-{{ scopeName }}"> \
<input type="hidden" name="scopeName" value="{{ scopeName }}" /> \
<div id="controlFilterPopoverDate" class="control-filter-popover control-filter-box-popover"> \
<div class="filter-search loading-indicator-container size-input-text"> \
<div class="field-datepicker"> \
<div class="input-with-icon right-align"> \
<i class="icon icon-calendar-o"></i> \
<input \
type="text" \
name="date" \
value="{{ date }}" \
class="form-control align-right popup-allow-focus" \
autocomplete="off" \
placeholder="{{ date_placeholder }}" /> \
</div> \
</div> \
<div class="filter-buttons"> \
<button class="btn btn-block btn-secondary" data-filter-action="clear"> \
{{ reset_button_text }} \
</button> \
</div> \
</div> \
</div> \
</form> \
'
}
/*
* Get popover range template
*/
FilterWidget.prototype.getPopoverRangeTemplate = function () {
return ' \
<form id="controlFilterPopoverRange-{{ scopeName }}"> \
<input type="hidden" name="scopeName" value="{{ scopeName }}" /> \
<div id="controlFilterPopoverDate" class="control-filter-popover control-filter-box-popover --range"> \
<div class="filter-search loading-indicator-container size-input-text"> \
<div class="field-datepicker"> \
<div class="input-with-icon right-align"> \
<i class="icon icon-calendar-o"></i> \
<input \
type="text" \
name="date" \
value="{{ date }}" \
class="form-control align-right popup-allow-focus" \
autocomplete="off" \
placeholder="{{ after_placeholder }}" /> \
</div> \
</div> \
<div class="field-datepicker"> \
<div class="input-with-icon right-align"> \
<i class="icon icon-calendar-o"></i> \
<input \
type="text" \
name="date" \
value="{{ date }}" \
class="form-control align-right popup-allow-focus" \
autocomplete="off" \
placeholder="{{ before_placeholder }}" /> \
</div> \
</div> \
<div class="filter-buttons"> \
<button class="btn btn-block btn-primary" data-filter-action="filter"> \
{{ filter_button_text }} \
</button> \
<button class="btn btn-block btn-secondary" data-filter-action="clear"> \
{{ reset_button_text }} \
</button> \
</div> \
</div> \
</div> \
</form> \
'
}
FilterWidget.prototype.displayPopoverDate = function ($scope) {
var self = this,
scopeName = $scope.data('scope-name'),
data = this.scopeValues[scopeName]
data = $.extend({}, data, {
filter_button_text: this.getLang('filter.dates.filter_button_text'),
reset_button_text: this.getLang('filter.dates.reset_button_text'),
date_placeholder: this.getLang('filter.dates.date_placeholder', 'Date')
})
data.scopeName = scopeName
// Destroy any popovers already bound
$scope.data('oc.popover', null)
$scope.ocPopover({
content: Mustache.render(this.getPopoverDateTemplate(), data),
modal: false,
highlightModalTarget: true,
closeOnPageClick: true,
placement: 'bottom',
onCheckDocumentClickTarget: function (target) {
return self.onCheckDocumentClickTargetDatePicker(target)
}
})
}
FilterWidget.prototype.displayPopoverRange = function ($scope) {
var self = this,
scopeName = $scope.data('scope-name'),
data = this.scopeValues[scopeName]
data = $.extend({}, data, {
filter_button_text: this.getLang('filter.dates.filter_button_text'),
reset_button_text: this.getLang('filter.dates.reset_button_text'),
after_placeholder: this.getLang('filter.dates.after_placeholder', 'After'),
before_placeholder: this.getLang('filter.dates.before_placeholder', 'Before')
})
data.scopeName = scopeName
// Destroy any popovers already bound
$scope.data('oc.popover', null)
$scope.ocPopover({
content: Mustache.render(this.getPopoverRangeTemplate(), data),
modal: false,
highlightModalTarget: true,
closeOnPageClick: true,
placement: 'bottom',
onCheckDocumentClickTarget: function (target) {
return self.onCheckDocumentClickTargetDatePicker(target)
}
})
}
FilterWidget.prototype.initDatePickers = function (isRange) {
var self = this,
scopeData = this.$activeScope.data('scope-data'),
$inputs = $('.field-datepicker input', '#controlFilterPopoverDate'),
data = this.scopeValues[this.activeScopeName]
if (!data) {
data = {
dates: isRange ? (scopeData.dates ? scopeData.dates : []) : (scopeData.date ? [scopeData.date] : [])
}
}
$inputs.each(function (index, datepicker) {
var defaultValue = '',
$datepicker = $(datepicker),
defaults = {
minDate: new Date(scopeData.minDate),
maxDate: new Date(scopeData.maxDate),
firstDay: scopeData.firstDay,
yearRange: scopeData.yearRange,
setDefaultDate: '' !== defaultValue ? defaultValue.toDate() : '',
format: self.getDateFormat(),
i18n: self.getLang('datepicker')
}
if (0 <= index && index < data.dates.length) {
defaultValue = data.dates[index] ? moment.tz(data.dates[index], self.appTimezone).tz(self.timezone) : ''
}
if (!isRange) {
defaults.onSelect = function () {
self.filterByDate()
}
}
datepicker.value = '' !== defaultValue ? defaultValue.format(self.getDateFormat()) : '';
$datepicker.pikaday(defaults)
})
}
FilterWidget.prototype.clearDatePickers = function () {
var $inputs = $('.field-datepicker input', '#controlFilterPopoverDate')
$inputs.each(function (index, datepicker) {
var $datepicker = $(datepicker)
$datepicker.data('pikaday').destroy()
})
}
FilterWidget.prototype.updateScopeDateSetting = function ($scope, dates) {
var $setting = $scope.find('.filter-setting'),
dateFormat = this.getDateFormat(),
dateRegex =/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/,
reset = false
if (dates && dates.length) {
dates[0] = dates[0] && dates[0].match(dateRegex) ? dates[0] : null
if (dates.length > 1) {
dates[1] = dates[1] && dates[1].match(dateRegex) ? dates[1] : null
if(dates[0] || dates[1]) {
var after = dates[0] ? moment.tz(dates[0], this.appTimezone).tz(this.timezone).format(dateFormat) : '-∞',
before = dates[1] ? moment.tz(dates[1], this.appTimezone).tz(this.timezone).format(dateFormat) : '∞'
$setting.text(after + ' → ' + before)
} else {
reset = true
}
}
else if(dates[0]) {
$setting.text(moment.tz(dates[0], this.appTimezone).tz(this.timezone).format(dateFormat))
} else {
reset = true
}
}
else {
reset = true
}
if(reset) {
$setting.text(this.getLang('filter.dates.all', 'all'));
$scope.removeClass('active')
} else {
$scope.addClass('active')
}
}
FilterWidget.prototype.filterByDate = function (isReset) {
var self = this,
dates = []
if (!isReset) {
var datepickers = $('.field-datepicker input', '#controlFilterPopoverDate')
datepickers.each(function (index, datepicker) {
var date = $(datepicker).data('pikaday').toString('YYYY-MM-DD')
if(date.match(/\d{4}-\d{2}-\d{2}/)) {
if (index === 0) {
date += ' 00:00:00'
} else if (index === 1) {
date += ' 23:59:59'
}
date = moment.tz(date, self.timezone)
.tz(self.appTimezone)
.format('YYYY-MM-DD HH:mm:ss')
} else {
date = null
}
dates.push(date)
})
}
this.updateScopeDateSetting(this.$activeScope, dates);
this.scopeValues[this.activeScopeName] = {
dates: dates
}
this.isActiveScopeDirty = true;
this.$activeScope.data('oc.popover').hide()
}
FilterWidget.prototype.getDateFormat = function () {
if (this.locale) {
return moment()
.locale(this.locale)
.localeData()
.longDateFormat('l')
}
return 'YYYY-MM-DD'
}
FilterWidget.prototype.onCheckDocumentClickTargetDatePicker = function (target) {
var $target = $(target)
// If the click happens on a pikaday element, do not close the popover
return $target.hasClass('pika-next') ||
$target.hasClass('pika-prev') ||
$target.hasClass('pika-select') ||
$target.hasClass('pika-button') ||
$target.parents('.pika-table').length ||
$target.parents('.pika-title').length
}
FilterWidget.prototype.initRegion = function() {
this.locale = $('meta[name="backend-locale"]').attr('content')
this.timezone = $('meta[name="backend-timezone"]').attr('content')
this.appTimezone = $('meta[name="app-timezone"]').attr('content')
if (!this.appTimezone) {
this.appTimezone = 'UTC'
}
if (!this.timezone) {
this.timezone = 'UTC'
}
// Set both timezones to UTC to disable converting between them
if (this.ignoreTimezone) {
this.appTimezone = 'UTC'
this.timezone = 'UTC'
}
}
}(window.jQuery);

View File

@@ -0,0 +1,713 @@
/*
* Filter Widget
*
* Data attributes:
* - data-behavior="filter" - enables the filter plugin
*
* Dependences:
* - Winter Popover (winter.popover.js)
*
* Notes:
* Ideally this control would not depend on loader or the AJAX framework,
* then the Filter widget can use events to handle this business logic.
*
* Require:
* - mustache/mustache
* - modernizr/modernizr
* - storm/popover
*/
+function ($) { "use strict";
var FilterWidget = function (element, options) {
this.$el = $(element);
this.options = options || {}
this.scopeValues = {}
this.scopeAvailable = {}
this.$activeScope = null
this.activeScopeName = null
this.isActiveScopeDirty = false
/*
* Throttle dependency updating
*/
this.dependantUpdateInterval = 300
this.dependantUpdateTimers = {}
this.init()
}
FilterWidget.DEFAULTS = {
optionsHandler: null,
updateHandler: null
}
/*
* Get popover template
*/
FilterWidget.prototype.getPopoverTemplate = function() {
return ' \
<form id="filterPopover-{{ scopeName }}"> \
<input type="hidden" name="scopeName" value="{{ scopeName }}" /> \
<div id="controlFilterPopover" class="control-filter-popover control-filter-box-popover --range"> \
<div class="filter-search loading-indicator-container size-input-text"> \
<button class="close" data-dismiss="popover" type="button">&times;</button> \
<input \
type="text" \
name="search" \
autocomplete="off" \
class="filter-search-input form-control icon search popup-allow-focus" \
data-search /> \
<div class="filter-items"> \
<ul> \
{{#available}} \
<li data-item-id="{{id}}"><a href="javascript:;">{{name}}</a></li> \
{{/available}} \
{{#loading}} \
<li class="loading"><span></span></li> \
{{/loading}} \
</ul> \
</div> \
<div class="filter-active-items"> \
<ul> \
{{#active}} \
<li data-item-id="{{id}}"><a href="javascript:;">{{name}}</a></li> \
{{/active}} \
</ul> \
</div> \
<div class="filter-buttons"> \
<button class="btn btn-block btn-primary wn-icon-filter" data-filter-action="apply"> \
{{ apply_button_text }} \
</button> \
<button class="btn btn-block btn-secondary wn-icon-eraser" data-filter-action="clear"> \
{{ clear_button_text }} \
</button> \
</div> \
</div> \
</div> \
</form> \
'
}
FilterWidget.prototype.init = function() {
var self = this
this.bindDependants()
// Setup event handler on type: checkbox scopes
this.$el.on('change', '.filter-scope input[type="checkbox"]', function(){
var $scope = $(this).closest('.filter-scope')
if ($scope.hasClass('is-indeterminate')) {
self.switchToggle($(this))
}
else {
self.checkboxToggle($(this))
}
})
// Apply classes to type: checkbox scopes that are active from the server
$('.filter-scope input[type="checkbox"]', this.$el).each(function() {
$(this)
.closest('.filter-scope')
.toggleClass('active', $(this).is(':checked'))
})
// Setup click handler on type: group scopes
this.$el.on('click', 'a.filter-scope', function(){
var $scope = $(this),
scopeName = $scope.data('scope-name')
// Second click closes the filter scope
if ($scope.hasClass('filter-scope-open')) return
self.$activeScope = $scope
self.activeScopeName = scopeName
self.isActiveScopeDirty = false
self.displayPopover($scope)
$scope.addClass('filter-scope-open')
})
// Setup event handlers on type: group scopes' controls
this.$el.on('show.oc.popover', 'a.filter-scope', function(event){
self.focusSearch()
$(event.relatedTarget).on('click', '#controlFilterPopover .filter-items > ul > li', function(){
self.selectItem($(this))
})
$(event.relatedTarget).on('click', '#controlFilterPopover .filter-active-items > ul > li', function(){
self.selectItem($(this), true)
})
$(event.relatedTarget).on('ajaxDone', '#controlFilterPopover input.filter-search-input', function(event, context, data){
self.filterAvailable(data.scopeName, data.options.available)
})
$(event.relatedTarget).on('click', '#controlFilterPopover [data-filter-action="apply"]', function (e) {
e.preventDefault()
self.filterScope()
})
$(event.relatedTarget).on('click', '#controlFilterPopover [data-filter-action="clear"]', function (e) {
e.preventDefault()
self.filterScope(true)
})
$(event.relatedTarget).on('input', '#controlFilterPopover input[data-search]', function (e) {
self.searchQuery($(this))
})
})
// Setup event handler to apply selected options when closing the type: group scope popup
this.$el.on('hide.oc.popover', 'a.filter-scope', function(){
var $scope = $(this)
self.pushOptions(self.activeScopeName)
self.activeScopeName = null
self.$activeScope = null
// Second click closes the filter scope
setTimeout(function() { $scope.removeClass('filter-scope-open') }, 200)
})
// Setup click handler on type: button-group scopes
this.$el.on('click', '.filter-scope.button-group button', function (e) {
var $button = $(e.target),
$scope = $button.closest('.filter-scope'),
scopeName = $scope.data('scope-name'),
scopeValue = $button.data('scope-value'),
isActive = $button.hasClass('btn-primary'),
isRequired = $scope.data('scope-required') === true || $scope.data('scope-required') === 'true'
if (!isRequired && isActive) {
$button.removeClass('btn-primary').addClass('btn-default')
scopeValue = null
} else {
$scope.find('button').removeClass('btn-primary').addClass('btn-default')
$button.removeClass('btn-default').addClass('btn-primary')
}
// Track selected value
this.scopeValues[scopeName] = scopeValue
if (this.options.updateHandler) {
var data = {
scopeName: scopeName,
value: scopeValue
}
$.wn.stripeLoadIndicator.show()
this.$el.request(this.options.updateHandler, {
data: data
}).always(function () {
$.wn.stripeLoadIndicator.hide()
}).done(function () {
$scope.trigger('change.oc.filterScope')
})
}
}.bind(this))
// Setup change handler on type: dropdown scopes
this.$el.on('change', '.filter-scope.dropdown select', function (e) {
var $select = $(e.target),
$scope = $select.closest('.filter-scope'),
scopeName = $scope.data('scope-name'),
scopeValue = $select.val()
this.scopeValues[scopeName] = scopeValue
if (this.options.updateHandler) {
var data = {
scopeName: scopeName,
value: scopeValue
}
$.wn.stripeLoadIndicator.show()
this.$el.request(this.options.updateHandler, {
data: data
}).always(function () {
$.wn.stripeLoadIndicator.hide()
}).done(function () {
$scope.trigger('change.oc.filterScope')
$scope.toggleClass('active', !!scopeValue)
})
}
}.bind(this))
}
/*
* Bind dependant scopes
*/
FilterWidget.prototype.bindDependants = function() {
if (!$('[data-scope-depends]', this.$el).length) {
return
}
var self = this,
scopeMap = {},
scopeElements = this.$el.find('.filter-scope')
/*
* Map master and slave scope
*/
scopeElements.filter('[data-scope-depends]').each(function() {
var name = $(this).data('scope-name'),
depends = $(this).data('scope-depends')
$.each(depends, function(index, depend){
if (!scopeMap[depend]) {
scopeMap[depend] = { scopes: [] }
}
scopeMap[depend].scopes.push(name)
})
})
/*
* When a master is updated, refresh its slaves
*/
$.each(scopeMap, function(scopeName, toRefresh){
scopeElements.filter('[data-scope-name="'+scopeName+'"]')
.on('change.oc.filterScope', $.proxy(self.onRefreshDependants, self, scopeName, toRefresh))
})
}
/*
* Refresh a dependancy scope
* Uses a throttle to prevent duplicate calls and click spamming
*/
FilterWidget.prototype.onRefreshDependants = function(scopeName, toRefresh) {
var self = this,
scopeElements = this.$el.find('.filter-scope')
if (this.dependantUpdateTimers[scopeName] !== undefined) {
window.clearTimeout(this.dependantUpdateTimers[scopeName])
}
this.dependantUpdateTimers[scopeName] = window.setTimeout(function() {
$.each(toRefresh.scopes, function (index, dependantScope) {
self.scopeValues[dependantScope] = null
var $scope = self.$el.find('[data-scope-name="'+dependantScope+'"]')
/*
* Request options from server
*/
self.$el.request(self.options.optionsHandler, {
data: { scopeName: dependantScope },
success: function(data) {
self.fillOptions(dependantScope, data.options)
self.updateScopeSetting($scope, data.options.active.length)
$scope.loadIndicator('hide')
}
})
})
}, this.dependantUpdateInterval)
$.each(toRefresh.scopes, function(index, scope) {
scopeElements.filter('[data-scope-name="'+scope+'"]')
.addClass('loading-indicator-container')
.loadIndicator()
})
}
FilterWidget.prototype.focusSearch = function() {
if (Modernizr.touchevents)
return
var $input = $('#controlFilterPopover input.filter-search-input'),
length = $input.val().length
$input.focus()
$input.get(0).setSelectionRange(length, length)
}
FilterWidget.prototype.updateScopeSetting = function($scope, amount) {
var $setting = $scope.find('.filter-setting')
if (amount) {
$setting.text(amount)
$scope.addClass('active')
}
else {
$setting.text(this.getLang('filter.group.all', 'all'))
$scope.removeClass('active')
}
}
FilterWidget.prototype.selectItem = function($item, isDeselect) {
var $otherContainer = isDeselect
? $item.closest('.control-filter-popover').find('.filter-items:first > ul')
: $item.closest('.control-filter-popover').find('.filter-active-items:first > ul')
$item
.addClass('animate-enter')
.prependTo($otherContainer)
.one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function(){
$(this).removeClass('animate-enter')
})
if (!this.scopeValues[this.activeScopeName])
return
var
itemId = $item.data('item-id'),
active = this.scopeValues[this.activeScopeName],
available = this.scopeAvailable[this.activeScopeName],
fromItems = isDeselect ? active : available,
toItems = isDeselect ? available : active,
testFunc = function(active){ return active.id == itemId },
item = ($.grep(fromItems, testFunc).pop() ?? {'id': itemId, 'name': $item.text()}),
filtered = $.grep(fromItems, testFunc, true)
if (isDeselect) {
this.scopeValues[this.activeScopeName] = filtered
this.scopeAvailable[this.activeScopeName].push(item)
} else {
this.scopeAvailable[this.activeScopeName] = filtered
this.scopeValues[this.activeScopeName].push(item)
}
this.toggleFilterButtons(active)
this.updateScopeSetting(this.$activeScope, isDeselect ? filtered.length : active.length)
this.isActiveScopeDirty = true
this.focusSearch()
}
FilterWidget.prototype.displayPopover = function($scope) {
var self = this,
scopeName = $scope.data('scope-name'),
data = null,
isLoaded = true,
container = false
if (typeof this.scopeAvailable[scopeName] !== "undefined" && this.scopeAvailable[scopeName]) {
data = $.extend({}, data, {
available: this.scopeAvailable[scopeName],
active: this.scopeValues[scopeName]
})
}
// If the filter is running in a modal, popovers should be
// attached to the modal container. This prevents z-index issues.
var modalParent = $scope.parents('.modal-dialog')
if (modalParent.length > 0) {
container = modalParent[0]
}
if (!data) {
data = { loading: true }
isLoaded = false
}
data = $.extend({}, data, {
apply_button_text: this.getLang('filter.scopes.apply_button_text', 'Apply'),
clear_button_text: this.getLang('filter.scopes.clear_button_text', 'Clear')
})
data.scopeName = scopeName
data.optionsHandler = self.options.optionsHandler
// Destroy any popovers already bound
$scope.data('oc.popover', null)
$scope.ocPopover({
content: Mustache.render(self.getPopoverTemplate(), data),
modal: false,
highlightModalTarget: true,
closeOnPageClick: true,
placement: 'bottom',
container: container
})
this.toggleFilterButtons()
// Load options for the first time
if (!isLoaded) {
self.loadOptions(scopeName)
}
}
/*
* Returns false if loading options is instant,
* otherwise returns a deferred promise object.
*/
FilterWidget.prototype.loadOptions = function(scopeName) {
var self = this,
data = { scopeName: scopeName }
/*
* Dataset provided manually
*/
var populated = this.$el.data('filterScopes')
if (populated && populated[scopeName]) {
self.fillOptions(scopeName, populated[scopeName])
return false
}
/*
* Request options from server
*/
return this.$el.request(this.options.optionsHandler, {
data: data,
success: function(data) {
self.fillOptions(scopeName, data.options)
self.toggleFilterButtons()
}
})
}
FilterWidget.prototype.fillOptions = function(scopeName, data) {
if (this.scopeValues[scopeName])
return
if (!data.active) data.active = []
if (!data.available) data.available = []
this.scopeValues[scopeName] = data.active
this.scopeAvailable[scopeName] = data.available
// Do not render if scope has changed
if (scopeName != this.activeScopeName)
return
/*
* Inject available
*/
var container = $('#controlFilterPopover .filter-items > ul').empty()
this.addItemsToListElement(container, data.available)
/*
* Inject active
*/
var container = $('#controlFilterPopover .filter-active-items > ul')
this.addItemsToListElement(container, data.active)
}
FilterWidget.prototype.filterAvailable = function(scopeName, available) {
if (this.activeScopeName != scopeName)
return
if (!this.scopeValues[this.activeScopeName])
return
var
self = this,
filtered = [],
items = this.scopeValues[scopeName]
/*
* Ensure any active items do not appear in the search results
*/
if (items.length) {
var activeIds = []
$.each(items, function (key, obj) {
activeIds.push(obj.id)
})
filtered = $.grep(available, function(item) {
return $.inArray(item.id, activeIds) === -1
})
}
else {
filtered = available
}
var container = $('#controlFilterPopover .filter-items > ul').empty()
self.addItemsToListElement(container, filtered)
}
FilterWidget.prototype.addItemsToListElement = function($ul, items) {
$.each(items, function(key, obj){
var item = $('<li />').data({ 'item-id': obj.id })
.append($('<a />').prop({ 'href': 'javascript:;',}).text(obj.name))
$ul.append(item)
})
}
FilterWidget.prototype.toggleFilterButtons = function(data)
{
var items = $('#controlFilterPopover .filter-active-items > ul'),
buttonContainer = $('#controlFilterPopover .filter-buttons')
if (data) {
data.length > 0 ? buttonContainer.show() : buttonContainer.hide()
} else {
items.children().length > 0 ? buttonContainer.show() : buttonContainer.hide()
}
}
/*
* Saves the options to the update handler
*/
FilterWidget.prototype.pushOptions = function(scopeName) {
if (!this.isActiveScopeDirty || !this.options.updateHandler)
return
var self = this,
data = {
scopeName: scopeName,
options: JSON.stringify(this.scopeValues[scopeName])
}
$.wn.stripeLoadIndicator.show()
this.$el.request(this.options.updateHandler, {
data: data
}).always(function () {
$.wn.stripeLoadIndicator.hide()
}).done(function () {
// Trigger dependsOn updates on successful requests
self.$el.find('[data-scope-name="'+scopeName+'"]').trigger('change.oc.filterScope')
})
}
FilterWidget.prototype.checkboxToggle = function($el) {
var isChecked = $el.is(':checked'),
$scope = $el.closest('.filter-scope'),
scopeName = $scope.data('scope-name')
this.scopeValues[scopeName] = isChecked
if (this.options.updateHandler) {
var data = {
scopeName: scopeName,
value: isChecked
}
$.wn.stripeLoadIndicator.show()
this.$el.request(this.options.updateHandler, {
data: data
}).always(function(){
$.wn.stripeLoadIndicator.hide()
})
}
$scope.toggleClass('active', isChecked)
}
FilterWidget.prototype.switchToggle = function($el) {
var switchValue = $el.data('checked'),
$scope = $el.closest('.filter-scope'),
scopeName = $scope.data('scope-name')
this.scopeValues[scopeName] = switchValue
if (this.options.updateHandler) {
var data = {
scopeName: scopeName,
value: switchValue
}
$.wn.stripeLoadIndicator.show()
this.$el.request(this.options.updateHandler, {
data: data
}).always(function(){
$.wn.stripeLoadIndicator.hide()
})
}
$scope.toggleClass('active', !!switchValue)
}
FilterWidget.prototype.filterScope = function (isReset) {
var scopeName = this.$activeScope.data('scope-name')
if (isReset) {
this.scopeValues[scopeName] = null
this.scopeAvailable[scopeName] = null
this.isActiveScopeDirty = true
this.updateScopeSetting(this.$activeScope, 0)
}
this.pushOptions(scopeName)
this.isActiveScopeDirty = false
this.$activeScope.data('oc.popover').hide()
}
FilterWidget.prototype.getLang = function(name, defaultValue) {
if ($.oc === undefined || $.wn.lang === undefined) {
return defaultValue
}
return $.wn.lang.get(name, defaultValue)
}
FilterWidget.prototype.searchQuery = function ($el) {
if (this.dataTrackInputTimer !== undefined) {
window.clearTimeout(this.dataTrackInputTimer)
}
var self = this
this.dataTrackInputTimer = window.setTimeout(function () {
var
lastValue = $el.data('oc.lastvalue'),
thisValue = $el.val()
if (lastValue !== undefined && lastValue == thisValue) {
return
}
$el.data('oc.lastvalue', thisValue)
if (self.lastDataTrackInputRequest) {
self.lastDataTrackInputRequest.abort()
}
var data = {
scopeName: self.activeScopeName,
search: thisValue
}
$.wn.stripeLoadIndicator.show()
self.lastDataTrackInputRequest = self.$el.request(self.options.optionsHandler, {
data: data
}).success(function(data){
self.filterAvailable(self.activeScopeName, data.options.available)
self.toggleFilterButtons()
}).always(function(){
$.wn.stripeLoadIndicator.hide()
})
}, 300)
}
// FILTER WIDGET PLUGIN DEFINITION
// ============================
var old = $.fn.filterWidget
$.fn.filterWidget = function (option) {
var args = arguments,
result
this.each(function () {
var $this = $(this)
var data = $this.data('oc.filterwidget')
var options = $.extend({}, FilterWidget.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.filterwidget', (data = new FilterWidget(this, options)))
if (typeof option == 'string') result = data[option].call($this)
if (typeof result != 'undefined') return false
})
return result ? result : this
}
$.fn.filterWidget.Constructor = FilterWidget
// FILTER WIDGET NO CONFLICT
// =================
$.fn.filterWidget.noConflict = function () {
$.fn.filterWidget = old
return this
}
// FILTER WIDGET DATA-API
// ==============
$(document).render(function(){
$('[data-control="filterwidget"]').filterWidget();
})
}(window.jQuery);

View File

@@ -0,0 +1,317 @@
/*
* Filter Widget
*
* Data attributes:
* - data-behavior="filter" - enables the filter plugin
*
* Dependences:
* - Winter Popover (winter.popover.js)
*
* Notes:
* Ideally this control would not depend on loader or the AJAX framework,
* then the Filter widget can use events to handle this business logic.
*
* Require:
* - mustache/mustache
* - modernizr/modernizr
* - storm/popover
*/
+function ($) {
"use strict";
var FilterWidget = $.fn.filterWidget.Constructor;
// OVERLOADED MODULE
// =================
var overloaded_init = FilterWidget.prototype.init;
FilterWidget.prototype.init = function () {
overloaded_init.apply(this)
this.initFilterNumber()
}
// NEW MODULE
// =================
FilterWidget.prototype.initFilterNumber = function () {
var self = this
this.$el.on('show.oc.popover', 'a.filter-scope-number', function (event) {
self.initNumberInputs($(this).hasClass('range'))
$(event.relatedTarget).on('click', '#controlFilterPopoverNum [data-filter-action="filter"]', function (e) {
e.preventDefault()
e.stopPropagation()
self.filterByNumber()
})
$(event.relatedTarget).on('click', '#controlFilterPopoverNum [data-filter-action="clear"]', function (e) {
e.preventDefault()
e.stopPropagation()
self.filterByNumber(true)
})
})
this.$el.on('hide.oc.popover', 'a.filter-scope-number', function () {
var $scope = $(this)
self.pushOptions(self.activeScopeName)
self.activeScopeName = null
self.$activeScope = null
// Second click closes the filter scope
setTimeout(function () {
$scope.removeClass('filter-scope-open')
}, 200)
})
this.$el.on('click', 'a.filter-scope-number', function () {
var $scope = $(this),
scopeName = $scope.data('scope-name')
// Ignore if already opened
if ($scope.hasClass('filter-scope-open')) return
// Ignore if another popover is opened
if (null !== self.activeScopeName) return
self.$activeScope = $scope
self.activeScopeName = scopeName
self.isActiveScopeDirty = false
if ($scope.hasClass('range')) {
self.displayPopoverNumberRange($scope)
}
else {
self.displayPopoverNumber($scope)
}
$scope.addClass('filter-scope-open')
})
}
/*
* Get popover number template
*/
FilterWidget.prototype.getPopoverNumberTemplate = function () {
return ' \
<form id="filterPopoverNumber-{{ scopeName }}"> \
<input type="hidden" name="scopeName" value="{{ scopeName }}" /> \
<div id="controlFilterPopoverNum" class="control-filter-popover control-filter-box-popover --range">\
<div class="filter-search loading-indicator-container size-input-text"> \
<div class="field-number"> \
<input \
type="number" \
name="number" \
value="{{ number }}" \
class="form-control align-right" \
autocomplete="off" \
placeholder="{{ number_placeholder }}" /> \
</div> \
<div class="filter-buttons"> \
<button class="btn btn-block btn-primary" data-filter-action="filter"> \
{{ filter_button_text }} \
</button> \
<button class="btn btn-block btn-secondary" data-filter-action="clear"> \
{{ reset_button_text }} \
</button> \
</div> \
</div> \
</div> \
</form> \
'
}
/*
* Get popover number range template
*/
FilterWidget.prototype.getPopoverNumberRangeTemplate = function () {
return ' \
<form id="filterPopoverNumberRange-{{ scopeName }}"> \
<input type="hidden" name="scopeName" value="{{ scopeName }}" /> \
<div id="controlFilterPopoverNum" class="control-filter-popover control-filter-box-popover --range"> \
<div class="filter-search loading-indicator-container size-input-text"> \
<div class="field-number"> \
<div class="right-align"> \
<input \
type="number" \
name="number" \
value="{{ number }}" \
class="form-control align-right" \
autocomplete="off" \
placeholder="{{ min_placeholder }}" /> \
</div> \
</div> \
<div class="field-number"> \
<div class="right-align"> \
<input \
type="number" \
{{ maxNumber }} \
name="number" \
value="{{ number }}" \
class="form-control align-right" \
autocomplete="off" \
placeholder="{{ max_placeholder }}" /> \
</div> \
</div> \
<div class="filter-buttons"> \
<button class="btn btn-block btn-primary" data-filter-action="filter"> \
{{ filter_button_text }} \
</button> \
<button class="btn btn-block btn-secondary" data-filter-action="clear"> \
{{ reset_button_text }} \
</button> \
</div> \
</div> \
</div> \
</form> \
'
}
FilterWidget.prototype.displayPopoverNumber = function ($scope) {
var self = this,
scopeName = $scope.data('scope-name'),
data = this.scopeValues[scopeName]
data = $.extend({}, data, {
filter_button_text: this.getLang('filter.numbers.filter_button_text'),
reset_button_text: this.getLang('filter.numbers.reset_button_text'),
number_placeholder: this.getLang('filter.numbers.number_placeholder', 'Number')
})
data.scopeName = scopeName
// Destroy any popovers already bound
$scope.data('oc.popover', null)
$scope.ocPopover({
content: Mustache.render(this.getPopoverNumberTemplate(), data),
modal: false,
highlightModalTarget: true,
closeOnPageClick: true,
placement: 'bottom',
})
}
FilterWidget.prototype.displayPopoverNumberRange = function ($scope) {
var self = this,
scopeName = $scope.data('scope-name'),
data = this.scopeValues[scopeName]
data = $.extend({}, data, {
filter_button_text: this.getLang('filter.numbers.filter_button_text'),
reset_button_text: this.getLang('filter.numbers.reset_button_text'),
min_placeholder: this.getLang('filter.numbers.min_placeholder', 'Min'),
max_placeholder: this.getLang('filter.numbers.max_placeholder', 'Max')
})
data.scopeName = scopeName
// Destroy any popovers already bound
$scope.data('oc.popover', null)
$scope.ocPopover({
content: Mustache.render(this.getPopoverNumberRangeTemplate(), data),
modal: false,
highlightModalTarget: true,
closeOnPageClick: true,
placement: 'bottom',
})
}
FilterWidget.prototype.initNumberInputs = function (isRange) {
var self = this,
scopeData = this.$activeScope.data('scope-data'),
$inputs = $('.field-number input', '#controlFilterPopoverNum'),
data = this.scopeValues[this.activeScopeName]
if (!data) {
data = {
numbers: isRange ? (scopeData.numbers ? scopeData.numbers : []) : (scopeData.number ? [scopeData.number] : [])
}
}
$inputs.each(function (index, numberinput) {
var defaultValue = ''
if (0 <= index && index < data.numbers.length) {
defaultValue = data.numbers[index] ? data.numbers[index] : ''
}
numberinput.value = '' !== defaultValue ? defaultValue : '';
if (scopeData.step) {
numberinput.step = scopeData.step
}
if (scopeData.minValue) {
numberinput.min = scopeData.minValue
}
if (scopeData.maxValue) {
numberinput.max = scopeData.maxValue
}
})
}
FilterWidget.prototype.updateScopeNumberSetting = function ($scope, numbers) {
var $setting = $scope.find('.filter-setting'),
numberRegex =/\d*/,
reset = false
if (numbers && numbers.length) {
numbers[0] = numbers[0] && numbers[0].match(numberRegex) ? numbers[0] : null
if (numbers.length > 1) {
numbers[1] = numbers[1] && numbers[1].match(numberRegex) ? numbers[1] : null
if(numbers[0] || numbers[1]) {
var min = numbers[0] ? numbers[0] : '-∞',
max = numbers[1] ? numbers[1] : '∞'
$setting.text(min + ' → ' + max)
} else {
reset = true
}
}
else if(numbers[0]) {
$setting.text(numbers[0])
} else {
reset = true
}
}
else {
reset = true
}
if(reset) {
$setting.text(this.getLang('filter.numbers.all', 'all'));
$scope.removeClass('active')
} else {
$scope.addClass('active')
}
}
FilterWidget.prototype.filterByNumber = function (isReset) {
var self = this,
numbers = []
if (!isReset) {
var numberinputs = $('.field-number input', '#controlFilterPopoverNum')
numberinputs.each(function (index, numberinput) {
var number = $(numberinput).val()
numbers.push(number)
})
}
this.updateScopeNumberSetting(this.$activeScope, numbers);
this.scopeValues[this.activeScopeName] = {
numbers: numbers
}
this.isActiveScopeDirty = true;
this.$activeScope.data('oc.popover').hide()
}
}(window.jQuery);

View File

@@ -0,0 +1,77 @@
/*
* The flash message.
*
* Documentation: ../docs/flashmessage.md
*
* Require:
* - bootstrap/transition
*/
+function ($) { "use strict";
var FlashMessage = function (options, el) {
var
options = $.extend({}, FlashMessage.DEFAULTS, options),
$element = $(el)
$('body > p.flash-message').remove()
if ($element.length == 0) {
$element = $('<p />').addClass(options.class).html(options.text)
}
$element.addClass('flash-message fade')
$element.attr('data-control', null)
$element.append('<button type="button" class="close" aria-hidden="true">&times;</button>')
$element.on('click', 'button', remove)
$element.on('click', remove)
$(document.body).append($element)
setTimeout(function() {
$element.addClass('in')
}, 100)
var timer = window.setTimeout(remove, options.interval * 1000)
function removeElement() {
$element.remove()
}
function remove() {
window.clearInterval(timer)
$element.removeClass('in')
$.support.transition && $element.hasClass('fade')
? $element
.one($.support.transition.end, removeElement)
.emulateTransitionEnd(500)
: removeElement()
}
}
FlashMessage.DEFAULTS = {
class: 'success',
text: 'Default text',
interval: 5
}
// FLASH MESSAGE PLUGIN DEFINITION
// ============================
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
$.wn.flashMsg = FlashMessage
// FLASH MESSAGE DATA-API
// ===============
$(document).render(function(){
$('[data-control=flash-message]').each(function(){
$.wn.flashMsg($(this).data(), this)
})
})
}(window.jQuery);

View File

@@ -0,0 +1,81 @@
/*
* Winter JavaScript foundation library.
*
* Base class for Winter CMS back-end classes.
*
* The class defines base functionality for dealing with memory management
* and cleaning up bound (proxied) methods.
*
* The base class defines the dispose method that cleans up proxied methods.
* If child classes implement their own dispose() method, they should call
* the base class dispose method (see the example below).
*
* Use the simple parasitic combination inheritance pattern to create child classes:
*
* var Base = $.wn.foundation.base,
* BaseProto = Base.prototype
*
* var SubClass = function(params) {
* // Call the parent constructor
* Base.call(this)
* }
*
* SubClass.prototype = Object.create(BaseProto)
* SubClass.prototype.constructor = SubClass
*
* // Child class methods can be defined only after the
* // prototype is updated in the two previous lines
*
* SubClass.prototype.dispose = function() {
* // Call the parent method
* BaseProto.dispose.call(this)
* };
*
* See:
*
* - https://developers.google.com/speed/articles/optimizing-javascript
* - http://javascriptissexy.com/oop-in-javascript-what-you-need-to-know/
* - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript
*
*/
+function ($) { "use strict";
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
if ($.wn.foundation === undefined)
$.wn.foundation = {}
$.wn.foundation._proxyCounter = 0
var Base = function() {
this.proxiedMethods = {}
}
Base.prototype.dispose = function() {
for (var key in this.proxiedMethods) {
this.proxiedMethods[key] = null
}
this.proxiedMethods = null
}
/*
* Creates a proxied method reference or returns an existing proxied method.
*/
Base.prototype.proxy = function(method) {
if (method.ocProxyId === undefined) {
$.wn.foundation._proxyCounter++
method.ocProxyId = $.wn.foundation._proxyCounter
}
if (this.proxiedMethods[method.ocProxyId] !== undefined)
return this.proxiedMethods[method.ocProxyId]
this.proxiedMethods[method.ocProxyId] = method.bind(this)
return this.proxiedMethods[method.ocProxyId]
}
$.wn.foundation.base = Base;
}(window.jQuery);

View File

@@ -0,0 +1,52 @@
/*
* Winter JavaScript foundation library.
*
* Utility functions for working back-end client-side UI controls.
*
* Usage examples:
*
* $.wn.foundation.controlUtils.markDisposable(el)
* $.wn.foundation.controlUtils.disposeControls(container)
*
*/
+function ($) { "use strict";
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
if ($.wn.foundation === undefined)
$.wn.foundation = {}
var ControlUtils = {
markDisposable: function(el) {
el.setAttribute('data-disposable', '')
},
/*
* Destroys all disposable controls in a container.
* The disposable controls should watch the dispose-control
* event.
*/
disposeControls: function(container) {
var controls = container.querySelectorAll('[data-disposable]')
for (var i=0, len=controls.length; i<len; i++)
$(controls[i]).triggerHandler('dispose-control')
if (container.hasAttribute('data-disposable'))
$(container).triggerHandler('dispose-control')
}
}
$.wn.foundation.controlUtils = ControlUtils;
$(document).on('ajaxBeforeReplace', function(ev){
// Automatically dispose controls in an element
// before the element contents is replaced.
// The ajaxBeforeReplace event is triggered in
// framework.js
$.wn.foundation.controlUtils.disposeControls(ev.target)
})
}(window.jQuery);

View File

@@ -0,0 +1,149 @@
/*
* Winter JavaScript foundation library.
*
* Light-weight utility functions for working with DOM elements. The functions
* work with elements directly, without jQuery, using the native JavaScript and DOM
* features.
*
* Usage examples:
*
* $.wn.foundation.element.addClass(myElement, myClass)
*
*/
+function ($) { "use strict";
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
if ($.wn.foundation === undefined)
$.wn.foundation = {}
var Element = {
hasClass: function(el, className) {
if (el.classList)
return el.classList.contains(className);
return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
},
addClass: function(el, className) {
var classes = className.split(' ')
for (var i = 0, len = classes.length; i < len; i++) {
var currentClass = classes[i].trim()
if (this.hasClass(el, currentClass))
return
if (el.classList)
el.classList.add(currentClass);
else
el.className += ' ' + currentClass;
}
},
removeClass: function(el, className) {
if (el.classList)
el.classList.remove(className);
else
el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
},
toggleClass: function(el, className, add) {
if (add === undefined) {
if (this.hasClass(el, className)) {
this.removeClass(el, className)
}
else {
this.addClass(el, className)
}
}
if (add && !this.hasClass(el, className)) {
this.addClass(el, className)
return
}
if (!add && this.hasClass(el, className)) {
this.removeClass(el, className)
return
}
},
/*
* Returns element absolution position.
* If the second parameter value is false, the scrolling
* won't be added to the result (which could improve the performance).
*/
absolutePosition: function(element, ignoreScrolling) {
var top = ignoreScrolling === true ? 0 : document.body.scrollTop,
left = 0
do {
top += element.offsetTop || 0;
if (ignoreScrolling !== true)
top -= element.scrollTop || 0
left += element.offsetLeft || 0
element = element.offsetParent
} while(element)
return {
top: top,
left: left
}
},
getCaretPosition: function(input) {
if (document.selection) {
var selection = document.selection.createRange()
selection.moveStart('character', -input.value.length)
return selection.text.length
}
if (input.selectionStart !== undefined)
return input.selectionStart
return 0
},
setCaretPosition: function(input, position) {
if (document.selection) {
var range = input.createTextRange()
setTimeout(function() {
// Asynchronous layout update, better performance
range.collapse(true)
range.moveStart("character", position)
range.moveEnd("character", 0)
range.select()
range = null
input = null
}, 0)
}
if (input.selectionStart !== undefined) {
setTimeout(function() {
// Asynchronous layout update
input.selectionStart = position
input.selectionEnd = position
input = null
}, 0)
}
},
elementContainsPoint: function(element, point) {
var elementPosition = $.wn.foundation.element.absolutePosition(element),
elementRight = elementPosition.left + element.offsetWidth,
elementBottom = elementPosition.top + element.offsetHeight
return point.x >= elementPosition.left && point.x <= elementRight
&& point.y >= elementPosition.top && point.y <= elementBottom
}
}
$.wn.foundation.element = Element;
}(window.jQuery);

View File

@@ -0,0 +1,83 @@
/*
* Winter JavaScript foundation library.
*
* Light-weight utility functions for working with native DOM events. The functions
* work with events directly, without jQuery, using the native JavaScript and DOM
* features.
*
* Usage examples:
*
* $.wn.foundation.event.stop(ev)
*
*/
+function ($) { "use strict";
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
if ($.wn.foundation === undefined)
$.wn.foundation = {}
var Event = {
/*
* Returns the event target element.
* If the second argument is provided (string), the function
* will try to find the first parent with the tag name matching
* the argument value.
*/
getTarget: function(ev, tag) {
var target = ev.target ? ev.target : ev.srcElement
if (tag === undefined)
return target
var tagName = target.tagName
while (tagName != tag) {
target = target.parentNode
if (!target)
return null
tagName = target.tagName
}
return target
},
stop: function(ev) {
if (ev.stopPropagation)
ev.stopPropagation()
else
ev.cancelBubble = true
if(ev.preventDefault)
ev.preventDefault()
else
ev.returnValue = false
},
pageCoordinates: function(ev) {
if (ev.pageX || ev.pageY) {
return {
x: ev.pageX,
y: ev.pageY
}
}
else if (ev.clientX || ev.clientY) {
return {
x: (ev.clientX + document.body.scrollLeft + document.documentElement.scrollLeft),
y: (ev.clientY + document.body.scrollTop + document.documentElement.scrollTop)
}
}
return {
x: 0,
y: 0
}
}
}
$.wn.foundation.event = Event;
}(window.jQuery);

View File

@@ -0,0 +1,223 @@
/*
* Hot key binding.
*
* Data attributes:
* - data-hotkey="ctrl+s, cmd+s" - enables the hotkey plugin
*
* JavaScript API:
*
* $('html').hotKey({ hotkey: 'ctrl+s, cmd+s', hotkeyVisible: false, callback: doSomething });
*/
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var HotKey = function (element, options) {
if (!options.hotkey) {
throw new Error('No hotkey has been defined.');
}
this.$el = $(element)
this.$target = $(options.hotkeyTarget)
this.options = options || {}
this.keyConditions = []
this.keyMap = null
$.wn.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
HotKey.prototype = Object.create(BaseProto)
HotKey.prototype.constructor = HotKey
HotKey.prototype.dispose = function() {
if (this.$el === null) {
return
}
this.unregisterHandlers()
this.$el.removeData('oc.hotkey')
this.$target = null
this.$el = null
this.keyConditions = null
this.keyMap = null
this.options = null
BaseProto.dispose.call(this)
}
HotKey.prototype.init = function() {
this.initKeyMap()
var keys = this.options.hotkey.toLowerCase().split(',')
for (var i = 0, len = keys.length; i < len; i++) {
var keysTrimmed = this.trim(keys[i])
this.keyConditions.push(this.makeCondition(keysTrimmed))
}
this.$target.on('keydown', this.proxy(this.onKeyDown))
this.$el.one('dispose-control', this.proxy(this.dispose))
}
HotKey.prototype.unregisterHandlers = function() {
this.$target.off('keydown', this.proxy(this.onKeyDown))
this.$el.off('dispose-control', this.proxy(this.dispose))
}
HotKey.prototype.makeCondition = function(keyBind) {
var condition = { shift: false, ctrl: false, cmd: false, alt: false, specific: -1 },
keys = keyBind.split('+')
for (var i = 0, len = keys.length; i < len; i++) {
switch (keys[i]) {
case 'shift':
condition.shift = true
break
case 'ctrl':
condition.ctrl = true
break
case 'command':
case 'cmd':
case 'meta':
condition.cmd = true
break
case 'alt':
case 'option':
condition.alt = true
break
}
}
condition.specific = this.keyMap[keys[keys.length-1]]
if (typeof (condition.specific) == 'undefined') {
condition.specific = keys[keys.length-1].toUpperCase().charCodeAt()
}
return condition
}
HotKey.prototype.initKeyMap = function() {
this.keyMap = {
'esc': 27,
'tab': 9,
'space': 32,
'return': 13,
'enter': 13,
'backspace': 8,
'scroll': 145,
'capslock': 20,
'numlock': 144,
'pause': 19,
'break': 19,
'insert': 45,
'home': 36,
'delete': 46,
'suppr': 46,
'end': 35,
'pageup': 33,
'pagedown': 34,
'left': 37,
'up': 38,
'right': 39,
'down': 40,
'f1': 112,
'f2': 113,
'f3': 114,
'f4': 115,
'f5': 116,
'f6': 117,
'f7': 118,
'f8': 119,
'f9': 120,
'f10': 121,
'f11': 122,
'f12': 123
}
}
HotKey.prototype.trim = function(str) {
return str
.replace(/^\s+/, "")
.replace(/\s+$/, "")
}
HotKey.prototype.testConditions = function(ev) {
for (var i = 0, len = this.keyConditions.length; i < len; i++) {
var condition = this.keyConditions[i]
if (ev.which === condition.specific
&& ev.originalEvent.shiftKey === condition.shift
&& ev.originalEvent.ctrlKey === condition.ctrl
&& ev.originalEvent.metaKey === condition.cmd
&& ev.originalEvent.altKey === condition.alt) {
return true
}
}
return false
}
HotKey.prototype.onKeyDown = function(ev) {
if (this.testConditions(ev)) {
if (this.options.hotkeyVisible && !this.$el.is(':visible')) {
return
}
if (this.options.callback) {
return this.options.callback(this.$el, ev.currentTarget, ev)
}
}
}
HotKey.DEFAULTS = {
hotkey: null,
hotkeyTarget: 'html',
hotkeyVisible: true,
callback: function(element) {
element.trigger('click')
return false
}
}
// HOTKEY PLUGIN DEFINITION
// ============================
var old = $.fn.hotKey
$.fn.hotKey = function (option) {
var args = arguments;
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.hotkey')
var options = $.extend({}, HotKey.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.hotkey', (data = new HotKey(this, options)))
if (typeof option == 'string') data[option].apply(data, args)
})
}
$.fn.hotKey.Constructor = HotKey
// HOTKEY NO CONFLICT
// =================
$.fn.hotKey.noConflict = function () {
$.fn.hotKey = old
return this
}
// HOTKEY DATA-API
// ==============
$(document).render(function() {
$('[data-hotkey]').hotKey()
})
}(window.jQuery);

View File

@@ -0,0 +1,160 @@
/*
* The form change monitor API.
*
* - Documentation: ../docs/input-monitor.md
*/
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var ChangeMonitor = function (element, options) {
this.$el = $(element);
this.paused = false
this.options = options || {}
$.wn.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
ChangeMonitor.prototype = Object.create(BaseProto)
ChangeMonitor.prototype.constructor = ChangeMonitor
ChangeMonitor.prototype.init = function() {
this.$el.on('change', this.proxy(this.change))
this.$el.on('unchange.oc.changeMonitor', this.proxy(this.unchange))
this.$el.on('pause.oc.changeMonitor', this.proxy(this.pause))
this.$el.on('resume.oc.changeMonitor', this.proxy(this.resume))
this.$el.on('keyup input paste', 'input:not(.ace_search_field), textarea:not(.ace_text-input)', this.proxy(this.onInputChange))
$('input:not([type=hidden]):not(.ace_search_field), textarea:not(.ace_text-input)', this.$el).each(function() {
$(this).data('oldval.oc.changeMonitor', $(this).val());
})
if (this.options.windowCloseConfirm)
$(window).on('beforeunload', this.proxy(this.onBeforeUnload))
this.$el.one('dispose-control', this.proxy(this.dispose))
this.$el.trigger('ready.oc.changeMonitor')
}
ChangeMonitor.prototype.dispose = function() {
if (this.$el === null)
return
this.unregisterHandlers()
this.$el.removeData('oc.changeMonitor')
this.$el = null
this.options = null
BaseProto.dispose.call(this)
}
ChangeMonitor.prototype.unregisterHandlers = function() {
this.$el.off('change', this.proxy(this.change))
this.$el.off('unchange.oc.changeMonitor', this.proxy(this.unchange))
this.$el.off('pause.oc.changeMonitor ', this.proxy(this.pause))
this.$el.off('resume.oc.changeMonitor ', this.proxy(this.resume))
this.$el.off('keyup input paste', 'input:not(.ace_search_field), textarea:not(.ace_text-input)', this.proxy(this.onInputChange))
this.$el.off('dispose-control', this.proxy(this.dispose))
if (this.options.windowCloseConfirm)
$(window).off('beforeunload', this.proxy(this.onBeforeUnload))
}
ChangeMonitor.prototype.change = function(ev, inputChange) {
if (this.paused)
return
if (ev.target.className === 'ace_search_field')
return
if (!inputChange) {
var type = $(ev.target).attr('type')
if (type === 'text' || type === 'password')
return
}
if (!this.$el.hasClass('oc-data-changed')) {
this.$el.trigger('changed.oc.changeMonitor')
this.$el.addClass('oc-data-changed')
}
}
ChangeMonitor.prototype.unchange = function() {
if (this.paused)
return
if (this.$el.hasClass('oc-data-changed')) {
this.$el.trigger('unchanged.oc.changeMonitor')
this.$el.removeClass('oc-data-changed')
}
}
ChangeMonitor.prototype.onInputChange = function(ev) {
if (this.paused)
return
var $el = $(ev.target)
if ($el.data('oldval.oc.changeMonitor') !== $el.val()) {
$el.data('oldval.oc.changeMonitor', $el.val());
this.change(ev, true);
}
}
ChangeMonitor.prototype.pause = function() {
this.paused = true
}
ChangeMonitor.prototype.resume = function() {
this.paused = false
}
ChangeMonitor.prototype.onBeforeUnload = function() {
if ($.contains(document.documentElement, this.$el.get(0)) && this.$el.hasClass('oc-data-changed'))
return this.options.windowCloseConfirm
}
ChangeMonitor.DEFAULTS = {
windowCloseConfirm: false
}
// CHANGEMONITOR PLUGIN DEFINITION
// ===============================
var old = $.fn.changeMonitor
$.fn.changeMonitor = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.changeMonitor')
var options = $.extend({}, ChangeMonitor.DEFAULTS, $this.data(), typeof option === 'object' && option)
if (!data) $this.data('oc.changeMonitor', (data = new ChangeMonitor(this, options)))
})
}
$.fn.changeMonitor.Constructor = ChangeMonitor
// CHANGEMONITOR NO CONFLICT
// ===============================
$.fn.changeMonitor.noConflict = function () {
$.fn.changeMonitor = old
return this
}
// CHANGEMONITOR DATA-API
// ===============================
$(document).render(function(){
$('[data-change-monitor]').changeMonitor()
})
}(window.jQuery);

View File

@@ -0,0 +1,362 @@
/*
* An input preset converter.
*
* The API allows to convert text entered into an element to a URL, slug or file name
* value in another input element.
*
* Supported data attributes:
* - data-input-preset: specifies a CSS selector for a source input element
* - data-input-preset-closest-parent: optional, specifies a CSS selector for a closest common parent
* for the source and destination input elements.
* - data-input-preset-type: specifies the conversion type. Supported values are:
* url, file, slug, camel.
* - data-input-preset-prefix-input: optional, prefixes the converted value with the value found
* in the supplied input element using a CSS selector.
* - data-input-preset-remove-words: optional, use removeList to filter stop words of source string.
*
* Example: <input type="text" id="name" value=""/>
* <input type="text"
* data-input-preset="#name"
* data-input-preset-type="file">
*
* JavaScript API:
* $('#filename').inputPreset({inputPreset: '#name', inputPresetType: 'file'})
*/
+function ($) { "use strict";
var VIETNAMESE_MAP = {
'Á': 'A', 'À': 'A', 'Ã': 'A', 'Ả': 'A', 'Ạ': 'A', 'Ắ': 'A', 'Ằ': 'A', 'Ẵ':
'A', 'Ẳ': 'A', 'Ặ': 'A', 'Ấ': 'A', 'Ầ': 'A', 'Ẫ': 'A', 'Ẩ': 'A', 'Ậ': 'A',
'Đ': 'D', 'É': 'E', 'È': 'E', 'Ẽ': 'E', 'Ẻ': 'E', 'Ẹ': 'E', 'Ế': 'E', 'Ề':
'E', 'Ễ': 'E', 'Ể': 'E', 'Ệ': 'E', 'Ó': 'O', 'Ò': 'O', 'Ỏ': 'O', 'Õ': 'O',
'Ọ': 'O', 'Ố': 'O', 'Ồ': 'O', 'Ổ': 'O', 'Ỗ': 'O', 'Ộ': 'O', 'Ơ': 'O', 'Ớ':
'O', 'Ờ': 'O', 'Ở': 'O', 'Ỡ': 'O', 'Ợ': 'O', 'Í': 'I', 'Ì': 'I', 'Ỉ': 'I',
'Ĩ': 'I', 'Ị': 'I', 'Ú': 'U', 'Ù': 'U', 'Ủ': 'U', 'Ũ': 'U', 'Ụ': 'U', 'Ư':
'U', 'Ứ': 'U', 'Ừ': 'U', 'Ử': 'U', 'Ữ': 'U', 'Ự': 'U', 'Ý': 'Y', 'Ỳ': 'Y',
'Ỷ': 'Y', 'Ỹ': 'Y', 'Ỵ': 'Y', 'á': 'a', 'à': 'a', 'ã': 'a', 'ả': 'a', 'ạ':
'a', 'ắ': 'a', 'ằ': 'a', 'ẵ': 'a', 'ẳ': 'a', 'ặ': 'a', 'ấ': 'a', 'ầ': 'a',
'ẫ': 'a', 'ẩ': 'a', 'ậ': 'a', 'đ': 'd', 'é': 'e', 'è': 'e', 'ẽ': 'e', 'ẻ':
'e', 'ẹ': 'e', 'ế': 'e', 'ề': 'e', 'ễ': 'e', 'ể': 'e', 'ệ': 'e', 'ó': 'o',
'ò': 'o', 'ỏ': 'o', 'õ': 'o', 'ọ': 'o', 'ố': 'o', 'ồ': 'o', 'ổ': 'o', 'ỗ':
'o', 'ộ': 'o', 'ơ': 'o', 'ớ': 'o', 'ờ': 'o', 'ở': 'o', 'ỡ': 'o', 'ợ': 'o',
'í': 'i', 'ì': 'i', 'ỉ': 'i', 'ĩ': 'i', 'ị': 'i', 'ú': 'u', 'ù': 'u', 'ủ':
'u', 'ũ': 'u', 'ụ': 'u', 'ư': 'u', 'ứ': 'u', 'ừ': 'u', 'ử': 'u', 'ữ': 'u',
'ự': 'u', 'ý': 'y', 'ỳ': 'y', 'ỷ': 'y', 'ỹ': 'y', 'ỵ': 'y'
},
LATIN_MAP = {
'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', 'Ç':
'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', 'Î': 'I',
'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö':
'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U',
'Ý': 'Y', 'Þ': 'TH', 'Ÿ': 'Y', 'ß': 'ss', 'à':'a', 'á':'a', 'â': 'a', 'ã':
'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e',
'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò':
'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ō': 'o',
'œ': 'oe', 'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ':
'th', 'ÿ': 'y'
},
LATIN_SYMBOLS_MAP = {
'©':'(c)'
},
GREEK_MAP = {
'α':'a', 'β':'b', 'γ':'g', 'δ':'d', 'ε':'e', 'ζ':'z', 'η':'h', 'θ':'8',
'ι':'i', 'κ':'k', 'λ':'l', 'μ':'m', 'ν':'n', 'ξ':'3', 'ο':'o', 'π':'p',
'ρ':'r', 'σ':'s', 'τ':'t', 'υ':'y', 'φ':'f', 'χ':'x', 'ψ':'ps', 'ω':'w',
'ά':'a', 'έ':'e', 'ί':'i', 'ό':'o', 'ύ':'y', 'ή':'h', 'ώ':'w', 'ς':'s',
'ϊ':'i', 'ΰ':'y', 'ϋ':'y', 'ΐ':'i',
'Α':'A', 'Β':'B', 'Γ':'G', 'Δ':'D', 'Ε':'E', 'Ζ':'Z', 'Η':'H', 'Θ':'8',
'Ι':'I', 'Κ':'K', 'Λ':'L', 'Μ':'M', 'Ν':'N', 'Ξ':'3', 'Ο':'O', 'Π':'P',
'Ρ':'R', 'Σ':'S', 'Τ':'T', 'Υ':'Y', 'Φ':'F', 'Χ':'X', 'Ψ':'PS', 'Ω':'W',
'Ά':'A', 'Έ':'E', 'Ί':'I', 'Ό':'O', 'Ύ':'Y', 'Ή':'H', 'Ώ':'W', 'Ϊ':'I',
'Ϋ':'Y'
},
TURKISH_MAP = {
'ş':'s', 'Ş':'S', 'ı':'i', 'İ':'I', 'ç':'c', 'Ç':'C', 'ü':'u', 'Ü':'U',
'ö':'o', 'Ö':'O', 'ğ':'g', 'Ğ':'G'
},
RUSSIAN_MAP = {
'а':'a', 'б':'b', 'в':'v', 'г':'g', 'д':'d', 'е':'e', 'ё':'yo', 'ж':'zh',
'з':'z', 'и':'i', 'й':'j', 'к':'k', 'л':'l', 'м':'m', 'н':'n', 'о':'o',
'п':'p', 'р':'r', 'с':'s', 'т':'t', 'у':'u', 'ф':'f', 'х':'h', 'ц':'c',
'ч':'ch', 'ш':'sh', 'щ':'shch', 'ъ':'', 'ы':'y', 'ь':'', 'э':'e', 'ю':'yu',
'я':'ya',
'А':'A', 'Б':'B', 'В':'V', 'Г':'G', 'Д':'D', 'Е':'E', 'Ё':'Yo', 'Ж':'Zh',
'З':'Z', 'И':'I', 'Й':'J', 'К':'K', 'Л':'L', 'М':'M', 'Н':'N', 'О':'O',
'П':'P', 'Р':'R', 'С':'S', 'Т':'T', 'У':'U', 'Ф':'F', 'Х':'H', 'Ц':'C',
'Ч':'Ch', 'Ш':'Sh', 'Щ':'Shch', 'Ъ':'', 'Ы':'Y', 'Ь':'', 'Э':'E', 'Ю':'Yu',
'Я':'Ya'
},
UKRAINIAN_MAP = {
'Є':'Ye', 'І':'I', 'Ї':'Yi', 'Ґ':'G', 'є':'ye', 'і':'i', 'ї':'yi', 'ґ':'g'
},
CZECH_MAP = {
'č':'c', 'ď':'d', 'ě':'e', 'ň': 'n', 'ř':'r', 'š':'s', 'ť':'t', 'ů':'u',
'ž':'z', 'Č':'C', 'Ď':'D', 'Ě':'E', 'Ň': 'N', 'Ř':'R', 'Š':'S', 'Ť':'T',
'Ů':'U', 'Ž':'Z'
},
POLISH_MAP = {
'ą':'a', 'ć':'c', 'ę':'e', 'ł':'l', 'ń':'n', 'ó':'o', 'ś':'s', 'ź':'z',
'ż':'z', 'Ą':'A', 'Ć':'C', 'Ę':'E', 'Ł':'L', 'Ń':'N', 'Ó':'O', 'Ś':'S',
'Ź':'Z', 'Ż':'Z'
},
LATVIAN_MAP = {
'ā':'a', 'č':'c', 'ē':'e', 'ģ':'g', 'ī':'i', 'ķ':'k', 'ļ':'l', 'ņ':'n',
'š':'s', 'ū':'u', 'ž':'z', 'Ā':'A', 'Č':'C', 'Ē':'E', 'Ģ':'G', 'Ī':'I',
'Ķ':'K', 'Ļ':'L', 'Ņ':'N', 'Š':'S', 'Ū':'U', 'Ž':'Z'
},
ARABIC_MAP = {
'أ':'a', 'ب':'b', 'ت':'t', 'ث': 'th', 'ج':'g', 'ح':'h', 'خ':'kh', 'د':'d',
'ذ':'th', 'ر':'r', 'ز':'z', 'س':'s', 'ش':'sh', 'ص':'s', 'ض':'d', 'ط':'t',
'ظ':'th', 'ع':'aa', 'غ':'gh', 'ف':'f', 'ق':'k', 'ك':'k', 'ل':'l', 'م':'m',
'ن':'n', 'ه':'h', 'و':'o', 'ي':'y'
},
PERSIAN_MAP = {
'آ':'a', 'ا':'a', 'پ':'p', 'چ':'ch', 'ژ':'zh', 'ک':'k', 'گ':'gh', 'ی':'y'
},
LITHUANIAN_MAP = {
'ą':'a', 'č':'c', 'ę':'e', 'ė':'e', 'į':'i', 'š':'s', 'ų':'u', 'ū':'u',
'ž':'z',
'Ą':'A', 'Č':'C', 'Ę':'E', 'Ė':'E', 'Į':'I', 'Š':'S', 'Ų':'U', 'Ū':'U',
'Ž':'Z'
},
SERBIAN_MAP = {
'ђ':'dj', 'ј':'j', 'љ':'lj', 'њ':'nj', 'ћ':'c', 'џ':'dz', 'đ':'d',
'Ђ':'Dj', 'Ј':'j', 'Љ':'Lj', 'Њ':'Nj', 'Ћ':'C', 'Џ':'Dz', 'Đ':'D'
},
AZERBAIJANI_MAP = {
'ç':'c', 'ə':'e', 'ğ':'g', 'ı':'i', 'ö':'o', 'ş':'s', 'ü':'u',
'Ç':'C', 'Ə':'E', 'Ğ':'G', 'İ':'I', 'Ö':'O', 'Ş':'S', 'Ü':'U'
},
ROMANIAN_MAP = {
'ă':'a', 'â':'a', 'î':'i', 'ș':'s', 'ț':'t',
'Ă':'A', 'Â':'A', 'Î':'I', 'Ș':'S', 'Ț':'T'
},
BELARUSIAN_MAP = {
'ў':'w', 'Ў':'W'
},
SPECIFIC_MAPS = {
'de': {
'Ä': 'AE', 'Ö': 'OE', 'Ü': 'UE',
'ä': 'ae', 'ö': 'oe', 'ü': 'ue'
}
},
ALL_MAPS = [
VIETNAMESE_MAP,
LATIN_MAP,
LATIN_SYMBOLS_MAP,
GREEK_MAP,
TURKISH_MAP,
RUSSIAN_MAP,
UKRAINIAN_MAP,
CZECH_MAP,
POLISH_MAP,
LATVIAN_MAP,
ARABIC_MAP,
PERSIAN_MAP,
LITHUANIAN_MAP,
SERBIAN_MAP,
AZERBAIJANI_MAP,
ROMANIAN_MAP,
BELARUSIAN_MAP
]
var removeList = [
"a", "an", "as", "at", "before", "but", "by", "for", "from", "is",
"in", "into", "like", "of", "off", "on", "onto", "per", "since",
"than", "the", "this", "that", "to", "up", "via", "with"
]
var locale = $('meta[name="backend-locale"]').attr('content')
var Downcoder = {
Initialize: function() {
if (Downcoder.map) {
return;
}
Downcoder.map = {};
Downcoder.chars = [];
if(typeof SPECIFIC_MAPS[locale] === 'object') {
ALL_MAPS.push(SPECIFIC_MAPS[locale]);
}
for (var i=0; i<ALL_MAPS.length; i++) {
var lookup = ALL_MAPS[i];
for (var c in lookup) {
if (lookup.hasOwnProperty(c)) {
Downcoder.map[c] = lookup[c];
}
}
}
for (var k in Downcoder.map) {
if (Downcoder.map.hasOwnProperty(k)) {
Downcoder.chars.push(k);
}
}
Downcoder.regex = new RegExp(Downcoder.chars.join('|'), 'g');
}
}
var InputPreset = function (element, options) {
var $el = this.$el = $(element)
this.options = options || {}
this.cancelled = false
var parent = options.inputPresetClosestParent !== undefined
? $el.closest(options.inputPresetClosestParent)
: undefined,
self = this,
prefix = ''
if (options.inputPresetPrefixInput !== undefined)
prefix = $(options.inputPresetPrefixInput, parent).val()
if (prefix === undefined)
prefix = ''
// Do not update the element if it already has a value and the value doesn't match the prefix
if ($el.val().length && $el.val() != prefix)
return
$el.val(prefix).trigger('oc.inputPreset.afterUpdate')
this.$src = $(options.inputPreset, parent)
this.$src.on('input paste', function(event) {
if (self.cancelled)
return
var timeout = event.type === 'paste' ? 100 : 0
var updateValue = function(self, el, prefix) {
if (el.data('update') === false) {
return
}
el
.val(prefix + self.formatValue())
.trigger('oc.inputPreset.afterUpdate')
}
var src = $(this)
setTimeout(function() {
$el.trigger('oc.inputPreset.beforeUpdate', [src])
setTimeout(updateValue, 100, self, $el, prefix)
}, timeout)
})
this.$el.on('change', function() {
self.cancelled = true
})
}
InputPreset.prototype.formatNamespace = function() {
var value = this.toCamel(this.$src.val())
return value.substr(0, 1).toUpperCase() + value.substr(1)
}
InputPreset.prototype.formatValue = function() {
if (this.options.inputPresetType == 'exact') {
return this.$src.val();
}
else if (this.options.inputPresetType == 'namespace') {
return this.formatNamespace()
}
if (this.options.inputPresetType == 'camel') {
var value = this.toCamel(this.$src.val(), this.$el.attr('maxlength'))
}
else {
var value = this.slugify(this.$src.val(), this.$el.attr('maxlength'))
}
if (this.options.inputPresetType == 'url') {
value = '/' + value
}
return value.replace(/\s/gi, "-")
}
InputPreset.prototype.toCamel = function(slug, numChars) {
Downcoder.Initialize()
slug = slug.replace(Downcoder.regex, function(m) {
return Downcoder.map[m]
})
slug = this.removeStopWords(slug);
slug = slug.toLowerCase()
slug = slug.replace(/(\b|-)\w/g, function(m) {
return m.toUpperCase();
});
slug = slug.replace(/[^-\w\s]/g, '')
slug = slug.replace(/^\s+|\s+$/g, '')
slug = slug.replace(/[-\s]+/g, '')
slug = slug.substr(0, 1).toLowerCase() + slug.substr(1);
return slug.substring(0, numChars)
}
InputPreset.prototype.slugify = function(slug, numChars) {
Downcoder.Initialize()
slug = slug.replace(Downcoder.regex, function(m) {
return Downcoder.map[m]
})
slug = this.removeStopWords(slug);
slug = slug.replace(/[^-\w\s]/g, '')
slug = slug.replace(/^\s+|\s+$/g, '')
slug = slug.replace(/[-\s]+/g, '-')
slug = slug.toLowerCase()
return slug.substring(0, numChars)
}
InputPreset.prototype.removeStopWords = function(str) {
if (this.options.inputPresetRemoveWords) {
var regex = new RegExp('\\b(' + removeList.join('|') + ')\\b', 'gi')
str = str.replace(regex, '')
}
return str;
}
InputPreset.DEFAULTS = {
inputPreset: '',
inputPresetType: 'slug',
inputPresetClosestParent: undefined,
inputPresetPrefixInput: undefined,
inputPresetRemoveWords: true
}
// INPUT CONVERTER PLUGIN DEFINITION
// ============================
var old = $.fn.inputPreset
$.fn.inputPreset = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.inputPreset')
var options = $.extend({}, InputPreset.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.inputPreset', (data = new InputPreset(this, options)))
})
}
$.fn.inputPreset.Constructor = InputPreset
// INPUT CONVERTER NO CONFLICT
// =================
$.fn.inputPreset.noConflict = function () {
$.fn.inputPreset = old
return this
}
// INPUT CONVERTER DATA-API
// ===============
$(document).render(function() {
$('[data-input-preset]').inputPreset()
})
}(window.jQuery);

View File

@@ -0,0 +1,195 @@
/*
* The trigger API
*
* - Documentation: ../docs/input-trigger.md
*/
+function ($) { "use strict";
var TriggerOn = function (element, options) {
var $el = this.$el = $(element);
this.options = options || {};
if (this.options.triggerCondition === false)
throw new Error('Trigger condition is not specified.')
if (this.options.trigger === false)
throw new Error('Trigger selector is not specified.')
if (this.options.triggerAction === false)
throw new Error('Trigger action is not specified.')
this.triggerCondition = this.options.triggerCondition
if (this.options.triggerCondition.indexOf('value') == 0) {
var match = this.options.triggerCondition.match(/[^[\]]+(?=])/g)
this.triggerCondition = 'value'
this.triggerConditionValue = (match) ? match : [""]
}
this.triggerParent = undefined
if (this.options.triggerClosestParent !== undefined) {
var closestParentElements = this.options.triggerClosestParent.split(',')
for (var i = 0; i < closestParentElements.length; i++) {
var $triggerElement = $el.closest(closestParentElements[i])
if ($triggerElement.length) {
this.triggerParent = $triggerElement
break
}
}
}
if (
this.triggerCondition == 'checked' ||
this.triggerCondition == 'unchecked' ||
this.triggerCondition == 'value'
) {
$(document).on('change', this.options.trigger, $.proxy(this.onConditionChanged, this))
}
var self = this
$el.on('oc.triggerOn.update', function(e){
e.stopPropagation()
self.onConditionChanged()
})
self.onConditionChanged()
}
TriggerOn.prototype.onConditionChanged = function() {
if (this.triggerCondition == 'checked') {
this.updateTarget(!!$(this.options.trigger + ':checked', this.triggerParent).length)
}
else if (this.triggerCondition == 'unchecked') {
this.updateTarget(!$(this.options.trigger + ':checked', this.triggerParent).length)
}
else if (this.triggerCondition == 'value') {
var trigger, triggered = false
trigger = $(this.options.trigger, this.triggerParent)
.not('input[type=checkbox], input[type=radio], input[type=button], input[type=submit]')
if (!trigger.length) {
trigger = $(this.options.trigger, this.triggerParent)
.not(':not(input[type=checkbox]:checked, input[type=radio]:checked)')
}
var self = this
trigger.each(function() {
var triggerValue = $(this).val();
$.each($.isArray(triggerValue) ? triggerValue : [triggerValue], function(key, val) {
triggered = $.inArray(val, self.triggerConditionValue) != -1
return !triggered
})
return !triggered
})
this.updateTarget(triggered)
}
}
TriggerOn.prototype.updateTarget = function(status) {
var self = this,
actions = this.options.triggerAction.split('|')
$.each(actions, function(index, action) {
self.updateTargetAction(action, status)
})
$(window).trigger('resize')
this.$el.trigger('oc.triggerOn.afterUpdate', status)
}
TriggerOn.prototype.updateTargetAction = function(action, status) {
if (action == 'show') {
this.$el
.toggleClass('hide', !status)
.trigger('hide.oc.triggerapi', [!status])
}
else if (action == 'hide') {
this.$el
.toggleClass('hide', status)
.trigger('hide.oc.triggerapi', [status])
}
else if (action == 'enable') {
this.$el
.prop('disabled', !status)
.toggleClass('control-disabled', !status)
.trigger('disable.oc.triggerapi', [!status])
}
else if (action == 'disable') {
this.$el
.prop('disabled', status)
.toggleClass('control-disabled', status)
.trigger('disable.oc.triggerapi', [status])
}
else if (action == 'empty' && status) {
this.$el
.not('input[type=checkbox], input[type=radio], input[type=button], input[type=submit]')
.val('')
this.$el
.not(':not(input[type=checkbox], input[type=radio])')
.prop('checked', false)
this.$el
.trigger('empty.oc.triggerapi')
.trigger('change')
}
if (action == 'show' || action == 'hide') {
this.fixButtonClasses()
}
}
TriggerOn.prototype.fixButtonClasses = function() {
var group = this.$el.closest('.btn-group')
if (group.length > 0 && this.$el.is(':last-child'))
this.$el.prev().toggleClass('last', this.$el.hasClass('hide'))
}
TriggerOn.DEFAULTS = {
triggerAction: false,
triggerCondition: false,
triggerClosestParent: undefined,
trigger: false
}
// TRIGGERON PLUGIN DEFINITION
// ============================
var old = $.fn.triggerOn
$.fn.triggerOn = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.triggerOn')
var options = $.extend({}, TriggerOn.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.triggerOn', (data = new TriggerOn(this, options)))
})
}
$.fn.triggerOn.Constructor = TriggerOn
// TRIGGERON NO CONFLICT
// =================
$.fn.triggerOn.noConflict = function () {
$.fn.triggerOn = old
return this
}
// TRIGGERON DATA-API
// ===============
$(document).render(function(){
$('[data-trigger]').triggerOn()
})
}(window.jQuery);

View File

@@ -0,0 +1,163 @@
/*
* Inspector data interaction class.
*
* Provides methods for loading and writing Inspector configuration
* and values form and to inspectable elements.
*/
+function ($) { "use strict";
// CLASS DEFINITION
// ============================
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var DataInteraction = function(element) {
this.element = element
Base.call(this)
}
DataInteraction.prototype = Object.create(BaseProto)
DataInteraction.prototype.constructor = Base
DataInteraction.prototype.dispose = function() {
this.element = null
BaseProto.dispose.call(this)
}
DataInteraction.prototype.getElementValuesInput = function() {
return this.element.querySelector('input[data-inspector-values]')
}
DataInteraction.prototype.normalizePropertyCode = function(code, configuration) {
var lowerCaseCode = code.toLowerCase()
for (var index in configuration) {
var propertyInfo = configuration[index]
if (propertyInfo.property.toLowerCase() == lowerCaseCode) {
return propertyInfo.property
}
}
return code
}
DataInteraction.prototype.loadValues = function(configuration) {
var valuesField = this.getElementValuesInput()
if (valuesField) {
var valuesStr = $.trim(valuesField.value)
try {
return valuesStr.length === 0 ? {} : JSON.parse(valuesStr)
}
catch (err) {
throw new Error('Error parsing Inspector field values. ' + err)
}
}
var values = {},
attributes = this.element.attributes
for (var i=0, len = attributes.length; i < len; i++) {
var attribute = attributes[i],
matches = []
if (matches = attribute.name.match(/^data-property-(.*)$/)) {
// Important - values contained in data-property-xxx attributes are
// considered strings and never parsed with JSON. The use of the
// data-property-xxx attributes is very limited - they're only
// used in Pages for creating snippets from partials, where properties
// are created with a table UI widget, which doesn't allow creating
// properties of any complex types.
//
// There is no a technically reliable way to determine when a string
// is a JSON data or a regular string. Users can enter a value
// like [10], which is a proper JSON value, but meant to be a string.
//
// One possible way to resolve it, if to check the property type loaded
// from the configuration and see if the corresponding editor expects
// complex data.
var normalizedPropertyName = normalizePropertyCode(matches[1], configuration)
values[normalizedPropertyName] = attribute.value
}
}
return values
}
DataInteraction.prototype.loadConfiguration = function(onComplete) {
var configurationField = this.element.querySelector('input[data-inspector-config]'),
result = {
configuration: {},
title: null,
description: null
},
$element = $(this.element)
result.title = $element.data('inspector-title')
result.description = $element.data('inspector-description')
if (configurationField) {
result.configuration = this.parseConfiguration(configurationField.value)
onComplete(result, this)
return
}
var $form = $element.closest('form'),
data = $element.data(),
self = this
$.wn.stripeLoadIndicator.show()
$form.request('onGetInspectorConfiguration', {
data: data
}).done(function inspectorConfigurationRequestDoneClosure(data) {
self.configurartionRequestDone(data, onComplete, result)
}).always(function() {
$.wn.stripeLoadIndicator.hide()
})
}
//
// Internal methods
//
DataInteraction.prototype.parseConfiguration = function(configuration) {
if (!$.isArray(configuration) && !$.isPlainObject(configuration)) {
if ($.trim(configuration) === 0) {
return {}
}
try {
return JSON.parse(configuration)
}
catch(err) {
throw new Error('Error parsing Inspector configuration. ' + err)
}
}
else {
return configuration
}
}
DataInteraction.prototype.configurartionRequestDone = function(data, onComplete, result) {
result.configuration = this.parseConfiguration(data.configuration.properties)
if (data.configuration.title !== undefined) {
result.title = data.configuration.title
}
if (data.configuration.description !== undefined) {
result.description = data.configuration.description
}
onComplete(result, this)
}
$.wn.inspector.dataInteraction = DataInteraction
}(window.jQuery);

View File

@@ -0,0 +1,256 @@
/*
* Inspector autocomplete editor class.
*
* Depends on winter.autocomplete.js
*/
+function ($) { "use strict";
var Base = $.wn.inspector.propertyEditors.string,
BaseProto = Base.prototype
var AutocompleteEditor = function(inspector, propertyDefinition, containerCell, group) {
this.autoUpdateTimeout = null
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
AutocompleteEditor.prototype = Object.create(BaseProto)
AutocompleteEditor.prototype.constructor = Base
AutocompleteEditor.prototype.dispose = function() {
this.clearAutoUpdateTimeout()
this.removeAutocomplete()
BaseProto.dispose.call(this)
}
AutocompleteEditor.prototype.build = function() {
var container = document.createElement('div'),
editor = document.createElement('input'),
placeholder = this.propertyDefinition.placeholder !== undefined ? this.propertyDefinition.placeholder : '',
value = this.inspector.getPropertyValue(this.propertyDefinition.property)
editor.setAttribute('type', 'text')
editor.setAttribute('class', 'string-editor')
editor.setAttribute('placeholder', placeholder)
container.setAttribute('class', 'autocomplete-container')
if (value === undefined) {
value = this.propertyDefinition.default
}
if (value === undefined) {
value = ''
}
editor.value = value
$.wn.foundation.element.addClass(this.containerCell, 'text autocomplete')
container.appendChild(editor)
this.containerCell.appendChild(container)
if (this.propertyDefinition.items !== undefined) {
this.buildAutoComplete(this.propertyDefinition.items)
}
else {
this.loadDynamicItems()
}
}
AutocompleteEditor.prototype.buildAutoComplete = function(items) {
var input = this.getInput()
if (items === undefined) {
items = []
}
var $input = $(input),
autocomplete = $input.data('autocomplete')
if (!autocomplete) {
$input.autocomplete({
source: this.prepareItems(items),
matchWidth: true
})
}
else {
autocomplete.source = this.prepareItems(items)
}
}
AutocompleteEditor.prototype.removeAutocomplete = function() {
var input = this.getInput()
$(input).autocomplete('destroy')
}
AutocompleteEditor.prototype.prepareItems = function(items) {
var result = {}
if ($.isArray(items)) {
for (var i = 0, len = items.length; i < len; i++) {
result[items[i]] = items[i]
}
}
else {
result = items
}
return result
}
AutocompleteEditor.prototype.supportsExternalParameterEditor = function() {
return false
}
AutocompleteEditor.prototype.getContainer = function() {
return this.getInput().parentNode
}
AutocompleteEditor.prototype.registerHandlers = function() {
BaseProto.registerHandlers.call(this)
$(this.getInput()).on('change', this.proxy(this.onInputKeyUp))
}
AutocompleteEditor.prototype.unregisterHandlers = function() {
BaseProto.unregisterHandlers.call(this)
$(this.getInput()).off('change', this.proxy(this.onInputKeyUp))
}
AutocompleteEditor.prototype.saveDependencyValues = function() {
this.prevDependencyValues = this.getDependencyValues()
}
AutocompleteEditor.prototype.getDependencyValues = function() {
var result = ''
for (var i = 0, len = this.propertyDefinition.depends.length; i < len; i++) {
var property = this.propertyDefinition.depends[i],
value = this.inspector.getPropertyValue(property)
if (value === undefined) {
value = '';
}
result += property + ':' + value + '-'
}
return result
}
AutocompleteEditor.prototype.onInspectorPropertyChanged = function(property) {
if (!this.propertyDefinition.depends || this.propertyDefinition.depends.indexOf(property) === -1) {
return
}
this.clearAutoUpdateTimeout()
if (this.prevDependencyValues === undefined || this.prevDependencyValues != dependencyValues) {
this.autoUpdateTimeout = setTimeout(this.proxy(this.loadDynamicItems), 200)
}
}
AutocompleteEditor.prototype.clearAutoUpdateTimeout = function() {
if (this.autoUpdateTimeout !== null) {
clearTimeout(this.autoUpdateTimeout)
this.autoUpdateTimeout = null
}
}
//
// Dynamic items
//
AutocompleteEditor.prototype.showLoadingIndicator = function() {
$(this.getContainer()).loadIndicator()
}
AutocompleteEditor.prototype.hideLoadingIndicator = function() {
if (this.isDisposed()) {
return
}
var $container = $(this.getContainer())
$container.loadIndicator('hide')
$container.loadIndicator('destroy')
$container.removeClass('loading-indicator-container')
}
AutocompleteEditor.prototype.loadDynamicItems = function() {
if (this.isDisposed()) {
return
}
this.clearAutoUpdateTimeout()
var container = this.getContainer(),
data = this.getRootSurface().getValues(),
$form = $(container).closest('form')
$.wn.foundation.element.addClass(container, 'loading-indicator-container size-small')
this.showLoadingIndicator()
if (this.triggerGetItems(data) === false) {
return
}
data['inspectorProperty'] = this.getPropertyPath()
data['inspectorClassName'] = this.inspector.options.inspectorClass
$form.request('onInspectableGetOptions', {
data: data,
})
.done(this.proxy(this.itemsRequestDone))
.always(this.proxy(this.hideLoadingIndicator))
}
AutocompleteEditor.prototype.triggerGetItems = function(values) {
var $inspectable = this.getInspectableElement()
if (!$inspectable) {
return true
}
var itemsEvent = $.Event('autocompleteitems.oc.inspector')
$inspectable.trigger(itemsEvent, [{
values: values,
callback: this.proxy(this.itemsRequestDone),
property: this.inspector.getPropertyPath(this.propertyDefinition.property),
propertyDefinition: this.propertyDefinition
}])
if (itemsEvent.isDefaultPrevented()) {
return false
}
return true
}
AutocompleteEditor.prototype.itemsRequestDone = function(data) {
if (this.isDisposed()) {
// Handle the case when the asynchronous request finishes after
// the editor is disposed
return
}
this.hideLoadingIndicator()
var loadedItems = {}
if (data.options) {
for (var i = data.options.length-1; i >= 0; i--) {
loadedItems[data.options[i].value] = data.options[i].title
}
}
this.buildAutoComplete(loadedItems)
}
$.wn.inspector.propertyEditors.autocomplete = AutocompleteEditor
}(window.jQuery);

View File

@@ -0,0 +1,217 @@
/*
* Inspector editor base class.
*/
+function ($) { "use strict";
// NAMESPACES
// ============================
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
if ($.wn.inspector === undefined)
$.wn.inspector = {}
if ($.wn.inspector.propertyEditors === undefined)
$.wn.inspector.propertyEditors = {}
// CLASS DEFINITION
// ============================
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var BaseEditor = function(inspector, propertyDefinition, containerCell, group) {
this.inspector = inspector
this.propertyDefinition = propertyDefinition
this.containerCell = containerCell
this.containerRow = containerCell.parentNode
this.parentGroup = group
this.group = null // Group created by a grouped editor, for example by the set editor
this.childInspector = null
this.validationSet = null
this.disposed = false
Base.call(this)
this.init()
}
BaseEditor.prototype = Object.create(BaseProto)
BaseEditor.prototype.constructor = Base
BaseEditor.prototype.dispose = function() {
this.disposed = true // After this point editors can't rely on any DOM references
this.disposeValidation()
if (this.childInspector) {
this.childInspector.dispose()
}
this.inspector = null
this.propertyDefinition = null
this.containerCell = null
this.containerRow = null
this.childInspector = null
this.parentGroup = null
this.group = null
this.validationSet = null
BaseProto.dispose.call(this)
}
BaseEditor.prototype.init = function() {
this.build()
this.registerHandlers()
this.initValidation()
}
BaseEditor.prototype.build = function() {
return null
}
BaseEditor.prototype.isDisposed = function() {
return this.disposed
}
BaseEditor.prototype.registerHandlers = function() {
}
BaseEditor.prototype.onInspectorPropertyChanged = function(property, value) {
}
BaseEditor.prototype.notifyChildSurfacesPropertyChanged = function(property, value) {
if (!this.hasChildSurface()) {
return
}
this.childInspector.notifyEditorsPropertyChanged(property, value)
}
BaseEditor.prototype.focus = function() {
}
BaseEditor.prototype.hasChildSurface = function() {
return this.childInspector !== null
}
BaseEditor.prototype.getRootSurface = function() {
return this.inspector.getRootSurface()
}
BaseEditor.prototype.getPropertyPath = function() {
return this.inspector.getPropertyPath(this.propertyDefinition.property)
}
/**
* Updates displayed value in the editor UI. The value is already set
* in the Inspector and should be loaded from Inspector.
*/
BaseEditor.prototype.updateDisplayedValue = function(value) {
}
BaseEditor.prototype.getPropertyName = function() {
return this.propertyDefinition.property
}
BaseEditor.prototype.getUndefinedValue = function() {
return this.propertyDefinition.default === undefined ? undefined : this.propertyDefinition.default
}
BaseEditor.prototype.throwError = function(errorMessage) {
throw new Error(errorMessage + ' Property: ' + this.propertyDefinition.property)
}
BaseEditor.prototype.getInspectableElement = function() {
return this.getRootSurface().getInspectableElement()
}
BaseEditor.prototype.isEmptyValue = function(value) {
return value === undefined
|| value === null
|| (typeof value == 'object' && $.isEmptyObject(value) )
|| (typeof value == 'string' && $.trim(value).length === 0)
|| (Object.prototype.toString.call(value) === '[object Array]' && value.length === 0)
}
//
// Validation
//
BaseEditor.prototype.initValidation = function() {
this.validationSet = new $.wn.inspector.validationSet(this.propertyDefinition, this.propertyDefinition.property)
}
BaseEditor.prototype.disposeValidation = function() {
this.validationSet.dispose()
}
BaseEditor.prototype.getValueToValidate = function() {
return this.inspector.getPropertyValue(this.propertyDefinition.property)
}
BaseEditor.prototype.validate = function(silentMode) {
var value = this.getValueToValidate()
if (value === undefined) {
value = this.getUndefinedValue()
}
var validationResult = this.validationSet.validate(value)
if (validationResult !== null) {
if (!silentMode) {
$.wn.flashMsg({text: validationResult, 'class': 'error', 'interval': 5})
}
return false
}
return true
}
BaseEditor.prototype.markInvalid = function() {
$.wn.foundation.element.addClass(this.containerRow, 'invalid')
this.inspector.getGroupManager().markGroupRowInvalid(this.parentGroup, this.inspector.getRootTable())
this.inspector.getRootSurface().expandGroupParents(this.parentGroup)
this.focus()
}
//
// External editor
//
BaseEditor.prototype.supportsExternalParameterEditor = function() {
return true
}
BaseEditor.prototype.onExternalPropertyEditorHidden = function() {
}
//
// Grouping
//
BaseEditor.prototype.isGroupedEditor = function() {
return false
}
BaseEditor.prototype.initControlGroup = function() {
this.group = this.inspector.getGroupManager().createGroup(this.propertyDefinition.property, this.parentGroup)
}
BaseEditor.prototype.createGroupedRow = function(property) {
var row = this.inspector.buildRow(property, this.group),
groupedClass = this.inspector.getGroupManager().isGroupExpanded(this.group) ? 'expanded' : 'collapsed'
this.inspector.applyGroupLevelToRow(row, this.group)
$.wn.foundation.element.addClass(row, 'property')
$.wn.foundation.element.addClass(row, groupedClass)
return row
}
$.wn.inspector.propertyEditors.base = BaseEditor
}(window.jQuery);

View File

@@ -0,0 +1,109 @@
/*
* Inspector checkbox editor class.
*
* This editor is used in $.wn.inspector.propertyEditors.set class.
* If updates that affect references to this.inspector and propertyDefinition are done,
* the propertyEditors.set class implementation should be reviewed.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.propertyEditors.base,
BaseProto = Base.prototype
var CheckboxEditor = function(inspector, propertyDefinition, containerCell, group) {
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
CheckboxEditor.prototype = Object.create(BaseProto)
CheckboxEditor.prototype.constructor = Base
CheckboxEditor.prototype.dispose = function() {
this.unregisterHandlers()
BaseProto.dispose.call(this)
}
CheckboxEditor.prototype.build = function() {
var editor = document.createElement('input'),
container = document.createElement('div'),
value = this.inspector.getPropertyValue(this.propertyDefinition.property),
label = document.createElement('label'),
isChecked = false,
id = this.inspector.generateSequencedId()
container.setAttribute('tabindex', 0)
container.setAttribute('class', 'custom-checkbox nolabel')
editor.setAttribute('type', 'checkbox')
editor.setAttribute('value', '1')
editor.setAttribute('placeholder', 'placeholder')
editor.setAttribute('id', id)
label.setAttribute('for', id)
label.textContent = this.propertyDefinition.title
container.appendChild(editor)
container.appendChild(label)
if (value === undefined) {
if (this.propertyDefinition.default !== undefined) {
isChecked = this.normalizeCheckedValue(this.propertyDefinition.default)
}
}
else {
isChecked = this.normalizeCheckedValue(value)
}
editor.checked = isChecked
this.containerCell.appendChild(container)
}
CheckboxEditor.prototype.normalizeCheckedValue = function(value) {
if (value == '0' || value == 'false') {
return false
}
return value
}
CheckboxEditor.prototype.getInput = function() {
return this.containerCell.querySelector('input')
}
CheckboxEditor.prototype.focus = function() {
this.getInput().parentNode.focus()
}
CheckboxEditor.prototype.updateDisplayedValue = function(value) {
this.getInput().checked = this.normalizeCheckedValue(value)
}
CheckboxEditor.prototype.isEmptyValue = function(value) {
if (value === 0 || value === '0' || value === 'false') {
return true
}
return BaseProto.isEmptyValue.call(this, value)
}
CheckboxEditor.prototype.registerHandlers = function() {
var input = this.getInput()
input.addEventListener('change', this.proxy(this.onInputChange))
}
CheckboxEditor.prototype.unregisterHandlers = function() {
var input = this.getInput()
input.removeEventListener('change', this.proxy(this.onInputChange))
}
CheckboxEditor.prototype.onInputChange = function() {
var isChecked = this.getInput().checked
this.inspector.setPropertyValue(this.propertyDefinition.property, isChecked ? 1 : 0)
}
$.wn.inspector.propertyEditors.checkbox = CheckboxEditor
}(window.jQuery);

View File

@@ -0,0 +1,450 @@
/*
* Inspector dictionary editor class.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.propertyEditors.popupBase,
BaseProto = Base.prototype
var DictionaryEditor = function(inspector, propertyDefinition, containerCell, group) {
this.keyValidationSet = null
this.valueValidationSet = null
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
DictionaryEditor.prototype = Object.create(BaseProto)
DictionaryEditor.prototype.constructor = Base
DictionaryEditor.prototype.dispose = function() {
this.disposeValidators()
this.keyValidationSet = null
this.valueValidationSet = null
BaseProto.dispose.call(this)
}
DictionaryEditor.prototype.init = function() {
this.initValidators()
BaseProto.init.call(this)
}
DictionaryEditor.prototype.supportsExternalParameterEditor = function() {
return false
}
//
// Popup editor methods
//
DictionaryEditor.prototype.setLinkText = function(link, value) {
var value = value !== undefined ? value
: this.inspector.getPropertyValue(this.propertyDefinition.property)
if (value === undefined) {
value = this.propertyDefinition.default
}
if (value === undefined || $.isEmptyObject(value)) {
var placeholder = this.propertyDefinition.placeholder
if (placeholder !== undefined) {
$.wn.foundation.element.addClass(link, 'placeholder')
link.textContent = placeholder
}
else {
link.textContent = 'Items: 0'
}
}
else {
if (typeof value !== 'object') {
this.throwError('Object list value should be an object.')
}
var itemCount = this.getValueKeys(value).length
$.wn.foundation.element.removeClass(link, 'placeholder')
link.textContent = 'Items: ' + itemCount
}
}
DictionaryEditor.prototype.getPopupContent = function() {
return '<form> \
<div class="modal-header"> \
<button type="button" class="close" data-dismiss="popup">&times;</button> \
<h4 class="modal-title">{{property}}</h4> \
</div> \
<div class="modal-body"> \
<div class="control-toolbar"> \
<div class="toolbar-item"> \
<div class="btn-group"> \
<button type="button" class="btn btn-primary \
wn-icon-plus" \
data-cmd="create-item">Add</button> \
<button type="button" class="btn btn-default \
empty wn-icon-trash-o" \
data-cmd="delete-item"></button> \
</div> \
</div> \
</div> \
<div class="form-group"> \
<div class="inspector-dictionary-container"> \
<table class="headers"> \
<thead> \
<tr> \
<td>Key</td> \
<td>Value</td> \
</tr> \
</thead> \
</table> \
<div class="values"> \
<div class="control-scrollpad" \
data-control="scrollpad"> \
<div class="scroll-wrapper"> \
<table class=" \
no-offset-bottom \
inspector-dictionary-table"> \
</table> \
</div> \
</div> \
</div> \
</div> \
</div> \
</div> \
<div class="modal-footer"> \
<button type="submit" class="btn btn-primary">OK</button> \
<button type="button" class="btn btn-default" data-dismiss="popup">Cancel</button> \
</div> \
</form>'
}
DictionaryEditor.prototype.configurePopup = function(popup) {
this.buildItemsTable(popup.get(0))
this.focusFirstInput()
}
DictionaryEditor.prototype.handleSubmit = function($form) {
return this.applyValues()
}
//
// Building and row management
//
DictionaryEditor.prototype.buildItemsTable = function(popup) {
var table = popup.querySelector('table.inspector-dictionary-table'),
tbody = document.createElement('tbody'),
items = this.inspector.getPropertyValue(this.propertyDefinition.property),
titleProperty = this.propertyDefinition.titleProperty
if (items === undefined) {
items = this.propertyDefinition.default
}
if (items === undefined || this.getValueKeys(items).length === 0) {
var row = this.buildEmptyRow()
tbody.appendChild(row)
}
else {
for (var key in items) {
var row = this.buildTableRow(key, items[key])
tbody.appendChild(row)
}
}
table.appendChild(tbody)
this.updateScrollpads()
}
DictionaryEditor.prototype.buildTableRow = function(key, value) {
var row = document.createElement('tr'),
keyCell = document.createElement('td'),
valueCell = document.createElement('td')
this.createInput(keyCell, key)
this.createInput(valueCell, value)
row.appendChild(keyCell)
row.appendChild(valueCell)
return row
}
DictionaryEditor.prototype.buildEmptyRow = function() {
return this.buildTableRow(null, null)
}
DictionaryEditor.prototype.createInput = function(container, value) {
var input = document.createElement('input'),
controlContainer = document.createElement('div')
input.setAttribute('type', 'text')
input.setAttribute('class', 'form-control')
input.value = value
controlContainer.appendChild(input)
container.appendChild(controlContainer)
}
DictionaryEditor.prototype.setActiveCell = function(input) {
var activeCells = this.popup.querySelectorAll('td.active')
for (var i = activeCells.length-1; i >= 0; i--) {
$.wn.foundation.element.removeClass(activeCells[i], 'active')
}
var activeCell = input.parentNode.parentNode // input / div / td
$.wn.foundation.element.addClass(activeCell, 'active')
}
DictionaryEditor.prototype.createItem = function() {
var activeRow = this.getActiveRow(),
newRow = this.buildEmptyRow(),
tbody = this.getTableBody(),
nextSibling = activeRow ? activeRow.nextElementSibling : null
tbody.insertBefore(newRow, nextSibling)
this.focusAndMakeActive(newRow.querySelector('input'))
this.updateScrollpads()
}
DictionaryEditor.prototype.deleteItem = function() {
var activeRow = this.getActiveRow(),
tbody = this.getTableBody()
if (!activeRow) {
return
}
var nextRow = activeRow.nextElementSibling,
prevRow = activeRow.previousElementSibling
tbody.removeChild(activeRow)
var newSelectedRow = nextRow ? nextRow : prevRow
if (!newSelectedRow) {
newSelectedRow = this.buildEmptyRow()
tbody.appendChild(newSelectedRow)
}
this.focusAndMakeActive(newSelectedRow .querySelector('input'))
this.updateScrollpads()
}
DictionaryEditor.prototype.applyValues = function() {
var tbody = this.getTableBody(),
dataRows = tbody.querySelectorAll('tr'),
link = this.getLink(),
result = {}
for (var i = 0, len = dataRows.length; i < len; i++) {
var dataRow = dataRows[i],
keyInput = this.getRowInputByIndex(dataRow, 0),
valueInput = this.getRowInputByIndex(dataRow, 1),
key = $.trim(keyInput.value),
value = $.trim(valueInput.value)
if (key.length == 0 && value.length == 0) {
continue
}
if (key.length == 0) {
$.wn.flashMsg({text: 'The key cannot be empty.', 'class': 'error', 'interval': 3})
this.focusAndMakeActive(keyInput)
return false
}
if (value.length == 0) {
$.wn.flashMsg({text: 'The value cannot be empty.', 'class': 'error', 'interval': 3})
this.focusAndMakeActive(valueInput)
return false
}
if (result[key] !== undefined) {
$.wn.flashMsg({text: 'Keys should be unique.', 'class': 'error', 'interval': 3})
this.focusAndMakeActive(keyInput)
return false
}
var validationResult = this.keyValidationSet.validate(key)
if (validationResult !== null) {
$.wn.flashMsg({text: validationResult, 'class': 'error', 'interval': 5})
return false
}
validationResult = this.valueValidationSet.validate(value)
if (validationResult !== null) {
$.wn.flashMsg({text: validationResult, 'class': 'error', 'interval': 5})
return false
}
result[key] = value
}
this.inspector.setPropertyValue(this.propertyDefinition.property, result)
this.setLinkText(link, result)
}
//
// Helpers
//
DictionaryEditor.prototype.getValueKeys = function(value) {
var result = []
for (var key in value) {
result.push(key)
}
return result
}
DictionaryEditor.prototype.getActiveRow = function() {
var activeCell = this.popup.querySelector('td.active')
if (!activeCell) {
return null
}
return activeCell.parentNode
}
DictionaryEditor.prototype.getTableBody = function() {
return this.popup.querySelector('table.inspector-dictionary-table tbody')
}
DictionaryEditor.prototype.updateScrollpads = function() {
$('.control-scrollpad', this.popup).scrollpad('update')
}
DictionaryEditor.prototype.focusFirstInput = function() {
var input = this.popup.querySelector('td input')
if (input) {
input.focus()
this.setActiveCell(input)
}
}
DictionaryEditor.prototype.getEditorCell = function(cell) {
return cell.parentNode.parentNode // cell / div / td
}
DictionaryEditor.prototype.getEditorRow = function(cell) {
return cell.parentNode.parentNode.parentNode // cell / div / td / tr
}
DictionaryEditor.prototype.focusAndMakeActive = function(input) {
input.focus()
this.setActiveCell(input)
}
DictionaryEditor.prototype.getRowInputByIndex = function(row, index) {
return row.cells[index].querySelector('input')
}
//
// Navigation
//
DictionaryEditor.prototype.navigateDown = function(ev) {
var cell = this.getEditorCell(ev.currentTarget),
row = this.getEditorRow(ev.currentTarget),
nextRow = row.nextElementSibling
if (!nextRow) {
return
}
var newActiveEditor = nextRow.cells[cell.cellIndex].querySelector('input')
this.focusAndMakeActive(newActiveEditor)
}
DictionaryEditor.prototype.navigateUp = function(ev) {
var cell = this.getEditorCell(ev.currentTarget),
row = this.getEditorRow(ev.currentTarget),
prevRow = row.previousElementSibling
if (!prevRow) {
return
}
var newActiveEditor = prevRow.cells[cell.cellIndex].querySelector('input')
this.focusAndMakeActive(newActiveEditor)
}
//
// Validation
//
DictionaryEditor.prototype.initValidators = function() {
this.keyValidationSet = new $.wn.inspector.validationSet({
validation: this.propertyDefinition.validationKey
}, this.propertyDefinition.property+'.validationKey')
this.valueValidationSet = new $.wn.inspector.validationSet({
validation: this.propertyDefinition.validationValue
}, this.propertyDefinition.property+'.validationValue')
}
DictionaryEditor.prototype.disposeValidators = function() {
this.keyValidationSet.dispose()
this.valueValidationSet.dispose()
}
//
// Event handlers
//
DictionaryEditor.prototype.onPopupShown = function(ev, link, popup) {
BaseProto.onPopupShown.call(this,ev, link, popup )
popup.on('focus.inspector', 'td input', this.proxy(this.onFocus))
popup.on('keydown.inspector', 'td input', this.proxy(this.onKeyDown))
popup.on('click.inspector', '[data-cmd]', this.proxy(this.onCommand))
}
DictionaryEditor.prototype.onPopupHidden = function(ev, link, popup) {
popup.off('.inspector', 'td input')
popup.off('.inspector', '[data-cmd]', this.proxy(this.onCommand))
BaseProto.onPopupHidden.call(this, ev, link, popup)
}
DictionaryEditor.prototype.onFocus = function(ev) {
this.setActiveCell(ev.currentTarget)
}
DictionaryEditor.prototype.onCommand = function(ev) {
var command = ev.currentTarget.getAttribute('data-cmd')
switch (command) {
case 'create-item' :
this.createItem()
break;
case 'delete-item' :
this.deleteItem()
break;
}
}
DictionaryEditor.prototype.onKeyDown = function(ev) {
if (ev.key === 'ArrowDown') {
return this.navigateDown(ev)
}
else if (ev.key === 'ArrowUp') {
return this.navigateUp(ev)
}
}
$.wn.inspector.propertyEditors.dictionary = DictionaryEditor
}(window.jQuery);

View File

@@ -0,0 +1,455 @@
/*
* Inspector checkbox dropdown class.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.propertyEditors.base,
BaseProto = Base.prototype
var DropdownEditor = function(inspector, propertyDefinition, containerCell, group) {
this.indicatorContainer = null
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
DropdownEditor.prototype = Object.create(BaseProto)
DropdownEditor.prototype.constructor = Base
DropdownEditor.prototype.init = function() {
this.dynamicOptions = this.propertyDefinition.options ? false : true
this.initialization = false
BaseProto.init.call(this)
}
DropdownEditor.prototype.dispose = function() {
this.unregisterHandlers()
this.destroyCustomSelect()
this.indicatorContainer = null
BaseProto.dispose.call(this)
}
//
// Building
//
DropdownEditor.prototype.build = function() {
var select = document.createElement('select')
$.wn.foundation.element.addClass(this.containerCell, 'dropdown')
$.wn.foundation.element.addClass(select, 'custom-select')
if (!this.dynamicOptions) {
this.loadStaticOptions(select)
}
this.containerCell.appendChild(select)
this.initCustomSelect()
if (this.dynamicOptions) {
this.loadDynamicOptions(true)
}
}
DropdownEditor.prototype.formatSelectOption = function(state) {
if (!state.id)
return state.text; // optgroup
var option = state.element,
iconClass = option.getAttribute('data-icon'),
imageSrc = option.getAttribute('data-image')
if (iconClass) {
return '<i class="select-icon '+iconClass+'"></i> ' + state.text
}
if (imageSrc) {
return '<img class="select-image" src="'+imageSrc+'" alt="" /> ' + state.text
}
return state.text
}
DropdownEditor.prototype.createOption = function(select, title, value) {
var option = document.createElement('option')
if (title !== null) {
if (!$.isArray(title)) {
option.textContent = title
}
else {
if (title[1].indexOf('.') !== -1) {
option.setAttribute('data-image', title[1])
}
else {
option.setAttribute('data-icon', title[1])
}
option.textContent = title[0]
}
}
if (value !== null) {
option.value = value
}
select.appendChild(option)
}
DropdownEditor.prototype.createOptions = function(select, options) {
for (var value in options) {
this.createOption(select, options[value], value)
}
}
DropdownEditor.prototype.initCustomSelect = function() {
var select = this.getSelect()
var options = {
dropdownCssClass: 'ocInspectorDropdown'
}
if (this.propertyDefinition.emptyOption !== undefined) {
options.placeholder = this.propertyDefinition.emptyOption
}
if (this.propertyDefinition.placeholder !== undefined) {
options.placeholder = this.propertyDefinition.placeholder
}
options.templateResult = this.formatSelectOption
options.templateSelection = this.formatSelectOption
options.escapeMarkup = function(m) {
return m
}
$(select).select2(options)
if (!Modernizr.touchevents) {
this.indicatorContainer = $('.select2-container', this.containerCell)
this.indicatorContainer.addClass('loading-indicator-container size-small')
}
}
DropdownEditor.prototype.createPlaceholder = function(select) {
var placeholder = this.propertyDefinition.placeholder || this.propertyDefinition.emptyOption
if (placeholder !== undefined && !Modernizr.touchevents) {
this.createOption(select, null, null)
}
if (placeholder !== undefined && Modernizr.touchevents) {
this.createOption(select, placeholder, null)
}
}
//
// Helpers
//
DropdownEditor.prototype.getSelect = function() {
return this.containerCell.querySelector('select')
}
DropdownEditor.prototype.clearOptions = function(select) {
while (select.firstChild) {
select.removeChild(select.firstChild)
}
}
DropdownEditor.prototype.hasOptionValue = function(select, value) {
var options = select.children
for (var i = 0, len = options.length; i < len; i++) {
if (options[i].value == value) {
return true
}
}
return false
}
DropdownEditor.prototype.normalizeValue = function(value) {
if (!this.propertyDefinition.booleanValues) {
return value
}
var str = String(value)
if (str.length === 0) {
return ''
}
if (str === 'true') {
return true
}
return false
}
//
// Event handlers
//
DropdownEditor.prototype.registerHandlers = function() {
var select = this.getSelect()
$(select).on('change', this.proxy(this.onSelectionChange))
}
DropdownEditor.prototype.onSelectionChange = function() {
var select = this.getSelect()
this.inspector.setPropertyValue(this.propertyDefinition.property, this.normalizeValue(select.value), this.initialization)
}
DropdownEditor.prototype.onInspectorPropertyChanged = function(property) {
if (!this.propertyDefinition.depends || this.propertyDefinition.depends.indexOf(property) === -1) {
return
}
var dependencyValues = this.getDependencyValues()
if (this.prevDependencyValues === undefined || this.prevDependencyValues != dependencyValues) {
this.loadDynamicOptions()
}
}
DropdownEditor.prototype.onExternalPropertyEditorHidden = function() {
if (this.dynamicOptions) {
this.loadDynamicOptions(false)
}
}
//
// Editor API methods
//
DropdownEditor.prototype.updateDisplayedValue = function(value) {
var select = this.getSelect()
select.value = value
}
DropdownEditor.prototype.getUndefinedValue = function() {
// Return default value if the default value is defined
if (this.propertyDefinition.default !== undefined) {
return this.propertyDefinition.default
}
// Return undefined if there's a placeholder value
if (this.propertyDefinition.placeholder !== undefined) {
return undefined
}
// Otherwise - return the first value in the list
var select = this.getSelect()
if (select) {
return this.normalizeValue(select.value)
}
return undefined
}
DropdownEditor.prototype.isEmptyValue = function(value) {
if (this.propertyDefinition.booleanValues) {
if (value === '') {
return true
}
return false
}
return BaseProto.isEmptyValue.call(this, value)
}
//
// Disposing
//
DropdownEditor.prototype.destroyCustomSelect = function() {
var $select = $(this.getSelect())
if ($select.data('select2') != null) {
$select.select2('destroy')
}
}
DropdownEditor.prototype.unregisterHandlers = function() {
var select = this.getSelect()
$(select).off('change', this.proxy(this.onSelectionChange))
}
//
// Static options
//
DropdownEditor.prototype.loadStaticOptions = function(select) {
var value = this.inspector.getPropertyValue(this.propertyDefinition.property)
this.createPlaceholder(select)
this.createOptions(select, this.propertyDefinition.options)
if (value === undefined) {
value = this.propertyDefinition.default
}
select.value = value
}
//
// Dynamic options
//
DropdownEditor.prototype.loadDynamicOptions = function(initialization) {
var currentValue = this.inspector.getPropertyValue(this.propertyDefinition.property),
data = this.getRootSurface().getValues(),
self = this,
$form = $(this.getSelect()).closest('form'),
dependents = this.inspector.findDependentProperties(this.propertyDefinition.property)
if (this.inspector.options.parentContainer) {
// get parent container data values instead (ref. objectlist editor)
data = this.inspector.options.parentContainer.getValues()
}
if (currentValue === undefined) {
currentValue = this.propertyDefinition.default
}
var callback = function dropdownOptionsRequestDoneClosure(data) {
self.hideLoadingIndicator()
self.optionsRequestDone(data, currentValue, true)
if (dependents.length > 0) {
for (var i in dependents) {
var editor = self.inspector.findPropertyEditor(dependents[i])
if (editor && typeof editor.onInspectorPropertyChanged === 'function') {
editor.onInspectorPropertyChanged(self.propertyDefinition.property)
}
}
}
}
if (this.propertyDefinition.depends) {
this.saveDependencyValues()
}
data['inspectorProperty'] = this.getPropertyPath()
data['inspectorClassName'] = this.inspector.options.inspectorClass
this.showLoadingIndicator()
if (this.triggerGetOptions(data, callback) === false) {
return
}
$form.request('onInspectableGetOptions', {
data: data,
}).done(callback).always(
this.proxy(this.hideLoadingIndicator)
)
}
DropdownEditor.prototype.triggerGetOptions = function(values, callback) {
var $inspectable = this.getInspectableElement()
if (!$inspectable) {
return true
}
var optionsEvent = $.Event('dropdownoptions.oc.inspector')
$inspectable.trigger(optionsEvent, [{
values: values,
callback: callback,
property: this.inspector.getPropertyPath(this.propertyDefinition.property),
propertyDefinition: this.propertyDefinition
}])
if (optionsEvent.isDefaultPrevented()) {
return false
}
return true
}
DropdownEditor.prototype.saveDependencyValues = function() {
this.prevDependencyValues = this.getDependencyValues()
}
DropdownEditor.prototype.getDependencyValues = function() {
var result = ''
for (var i = 0, len = this.propertyDefinition.depends.length; i < len; i++) {
var property = this.propertyDefinition.depends[i],
value = this.getRootSurface().getPropertyValue(property)
if (value === undefined) {
value = '';
}
result += property + ':' + value + '-'
}
return result
}
DropdownEditor.prototype.showLoadingIndicator = function() {
if (!Modernizr.touchevents) {
this.indicatorContainer.loadIndicator()
}
}
DropdownEditor.prototype.hideLoadingIndicator = function() {
if (this.isDisposed()) {
return
}
if (!Modernizr.touchevents) {
this.indicatorContainer.loadIndicator('hide')
this.indicatorContainer.loadIndicator('destroy')
}
}
DropdownEditor.prototype.optionsRequestDone = function(data, currentValue, initialization) {
if (this.isDisposed()) {
// Handle the case when the asynchronous request finishes after
// the editor is disposed
return
}
var select = this.getSelect()
// Without destroying and recreating the custom select
// there could be detached DOM nodes.
this.destroyCustomSelect()
this.clearOptions(select)
this.initCustomSelect()
this.createPlaceholder(select)
if (data.options) {
for (var i = 0, len = data.options.length; i < len; i++) {
this.createOption(select, data.options[i].title, data.options[i].value)
}
}
if (this.hasOptionValue(select, currentValue)) {
select.value = currentValue
}
else {
select.selectedIndex = this.propertyDefinition.placeholder === undefined ? 0 : -1
}
this.initialization = initialization
$(select).trigger('change')
this.initialization = false
}
$.wn.inspector.propertyEditors.dropdown = DropdownEditor
}(window.jQuery);

View File

@@ -0,0 +1,140 @@
/*
* Inspector object editor class.
*
* This class uses other editors.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.propertyEditors.base,
BaseProto = Base.prototype
var ObjectEditor = function(inspector, propertyDefinition, containerCell, group) {
if (propertyDefinition.properties === undefined) {
this.throwError('The properties property should be specified in the object editor configuration.')
}
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
ObjectEditor.prototype = Object.create(BaseProto)
ObjectEditor.prototype.constructor = Base
ObjectEditor.prototype.init = function() {
this.initControlGroup()
BaseProto.init.call(this)
}
//
// Building
//
ObjectEditor.prototype.build = function() {
var currentRow = this.containerCell.parentNode,
inspectorContainer = document.createElement('div'),
options = {
enableExternalParameterEditor: false,
onChange: this.proxy(this.onInspectorDataChange),
inspectorClass: this.inspector.options.inspectorClass
},
values = this.inspector.getPropertyValue(this.propertyDefinition.property)
if (values === undefined) {
values = {}
}
this.childInspector = new $.wn.inspector.surface(inspectorContainer,
this.propertyDefinition.properties,
values,
this.inspector.getInspectorUniqueId() + '-' + this.propertyDefinition.property,
options,
this.inspector,
this.group,
this.propertyDefinition.property)
this.inspector.mergeChildSurface(this.childInspector, currentRow)
}
//
// Helpers
//
ObjectEditor.prototype.cleanUpValue = function(value) {
if (value === undefined || typeof value !== 'object') {
return undefined
}
if (this.propertyDefinition.ignoreIfPropertyEmpty === undefined) {
return value
}
return this.getValueOrRemove(value)
}
ObjectEditor.prototype.getValueOrRemove = function(value) {
if (this.propertyDefinition.ignoreIfPropertyEmpty === undefined) {
return value
}
var targetProperty = this.propertyDefinition.ignoreIfPropertyEmpty,
targetValue = value[targetProperty]
if (this.isEmptyValue(targetValue)) {
return $.wn.inspector.removedProperty
}
return value
}
//
// Editor API methods
//
ObjectEditor.prototype.supportsExternalParameterEditor = function() {
return false
}
ObjectEditor.prototype.isGroupedEditor = function() {
return true
}
ObjectEditor.prototype.getUndefinedValue = function() {
var result = {}
for (var i = 0, len = this.propertyDefinition.properties.length; i < len; i++) {
var propertyName = this.propertyDefinition.properties[i].property,
editor = this.childInspector.findPropertyEditor(propertyName)
if (editor) {
result[propertyName] = editor.getUndefinedValue()
}
}
return this.getValueOrRemove(result)
}
ObjectEditor.prototype.validate = function(silentMode) {
var values = this.childInspector.getValues()
if (this.cleanUpValue(values) === $.wn.inspector.removedProperty) {
// Ignore any validation rules if the object's required
// property is empty (ignoreIfPropertyEmpty)
return true
}
return this.childInspector.validate(silentMode)
}
//
// Event handlers
//
ObjectEditor.prototype.onInspectorDataChange = function(property, value) {
var values = this.childInspector.getValues()
this.inspector.setPropertyValue(this.propertyDefinition.property, this.cleanUpValue(values))
}
$.wn.inspector.propertyEditors.object = ObjectEditor
}(window.jQuery);

View File

@@ -0,0 +1,700 @@
/*
* Inspector object list editor class.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.propertyEditors.base,
BaseProto = Base.prototype
var ObjectListEditor = function(inspector, propertyDefinition, containerCell, group) {
this.currentRowInspector = null
this.popup = null
if (propertyDefinition.titleProperty === undefined) {
throw new Error('The titleProperty property should be specified in the objectList editor configuration. Property: ' + propertyDefinition.property)
}
if (propertyDefinition.itemProperties === undefined) {
throw new Error('The itemProperties property should be specified in the objectList editor configuration. Property: ' + propertyDefinition.property)
}
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
ObjectListEditor.prototype = Object.create(BaseProto)
ObjectListEditor.prototype.constructor = Base
ObjectListEditor.prototype.init = function() {
if (this.isKeyValueMode()) {
var keyProperty = this.getKeyProperty()
if (!keyProperty) {
throw new Error('Object list key property ' + this.propertyDefinition.keyProperty
+ ' is not defined in itemProperties. Property: ' + this.propertyDefinition.property)
}
}
BaseProto.init.call(this)
}
ObjectListEditor.prototype.dispose = function() {
this.unregisterHandlers()
this.removeControls()
this.currentRowInspector = null
this.popup = null
BaseProto.dispose.call(this)
}
ObjectListEditor.prototype.supportsExternalParameterEditor = function() {
return false
}
//
// Building
//
ObjectListEditor.prototype.build = function() {
var link = document.createElement('a')
$.wn.foundation.element.addClass(link, 'trigger')
link.setAttribute('href', '#')
this.setLinkText(link)
$.wn.foundation.element.addClass(this.containerCell, 'trigger-cell')
this.containerCell.appendChild(link)
}
ObjectListEditor.prototype.setLinkText = function(link, value) {
var value = value !== undefined && value !== null ? value
: this.inspector.getPropertyValue(this.propertyDefinition.property)
if (value === null) {
value = undefined
}
if (value === undefined) {
var placeholder = this.propertyDefinition.placeholder
if (placeholder !== undefined) {
$.wn.foundation.element.addClass(link, 'placeholder')
link.textContent = placeholder
}
else {
link.textContent = 'Items: 0'
}
}
else {
var itemCount = 0
if (!this.isKeyValueMode()) {
if (value.length === undefined) {
throw new Error('Object list value should be an array. Property: ' + this.propertyDefinition.property)
}
itemCount = value.length
}
else {
if (typeof value !== 'object') {
throw new Error('Object list value should be an object. Property: ' + this.propertyDefinition.property)
}
itemCount = this.getValueKeys(value).length
}
$.wn.foundation.element.removeClass(link, 'placeholder')
link.textContent = 'Items: ' + itemCount
}
}
ObjectListEditor.prototype.getPopupContent = function() {
return '<form> \
<div class="modal-header"> \
<button type="button" class="close" data-dismiss="popup">&times;</button> \
<h4 class="modal-title">{{property}}</h4> \
</div> \
<div> \
<div class="layout inspector-columns-editor"> \
<div class="layout-row"> \
<div class="layout-cell items-column"> \
<div class="layout-relative"> \
<div class="layout"> \
<div class="layout-row min-size"> \
<div class="control-toolbar toolbar-padded"> \
<div class="toolbar-item"> \
<div class="btn-group"> \
<button type="button" class="btn btn-primary \
wn-icon-plus" \
data-cmd="create-item">Add</button> \
<button type="button" class="btn btn-default \
empty wn-icon-trash-o" \
data-cmd="delete-item"></button> \
</div> \
</div> \
</div> \
</div> \
<div class="layout-row"> \
<div class="layout-cell"> \
<div class="layout-relative"> \
<div class="layout-absolute"> \
<div class="control-scrollpad" \
data-control="scrollpad"> \
<div class="scroll-wrapper"> \
<table class="table data \
no-offset-bottom \
inspector-table-list"> \
</table> \
</div> \
</div> \
</div> \
</div> \
</div> \
</div> \
</div> \
</div> \
</div> \
<div class="layout-cell"> \
<div class="layout-relative"> \
<div class="layout-absolute"> \
<div class="control-scrollpad" data-control="scrollpad"> \
<div class="scroll-wrapper inspector-wrapper"> \
<div data-inspector-container> \
</div> \
</div> \
</div> \
</div> \
</div> \
</div> \
</div> \
</div> \
</div> \
<div class="modal-footer"> \
<button type="submit" class="btn btn-primary">OK</button> \
<button type="button" class="btn btn-default" data-dismiss="popup">Cancel</button> \
</div> \
</form>'
}
ObjectListEditor.prototype.buildPopupContents = function(popup) {
this.buildItemsTable(popup)
}
ObjectListEditor.prototype.buildItemsTable = function(popup) {
var table = popup.querySelector('table'),
tbody = document.createElement('tbody'),
items = this.inspector.getPropertyValue(this.propertyDefinition.property),
titleProperty = this.propertyDefinition.titleProperty
if (items === undefined || this.getValueKeys(items).length === 0) {
var row = this.buildEmptyRow()
tbody.appendChild(row)
}
else {
var firstRow = undefined
for (var key in items) {
var item = items[key],
itemInspectorValue = this.addKeyProperty(key, item),
itemText = item[titleProperty],
row = this.buildTableRow(itemText, 'rowlink')
row.setAttribute('data-inspector-values', JSON.stringify(itemInspectorValue))
tbody.appendChild(row)
if (firstRow === undefined) {
firstRow = row
}
}
}
table.appendChild(tbody)
if (firstRow !== undefined) {
this.selectRow(firstRow, true)
}
this.updateScrollpads()
}
ObjectListEditor.prototype.buildEmptyRow = function() {
return this.buildTableRow('No items found', 'no-data', 'nolink')
}
ObjectListEditor.prototype.removeEmptyRow = function() {
var tbody = this.getTableBody(),
row = tbody.querySelector('tr.no-data')
if (row) {
tbody.removeChild(row)
}
}
ObjectListEditor.prototype.buildTableRow = function(text, rowClass, cellClass) {
var row = document.createElement('tr'),
cell = document.createElement('td')
cell.textContent = text
if (rowClass !== undefined) {
$.wn.foundation.element.addClass(row, rowClass)
}
if (cellClass !== undefined) {
$.wn.foundation.element.addClass(cell, cellClass)
}
row.appendChild(cell)
return row
}
ObjectListEditor.prototype.updateScrollpads = function() {
$('.control-scrollpad', this.popup).scrollpad('update')
}
//
// Built-in Inspector management
//
ObjectListEditor.prototype.selectRow = function(row, forceSelect) {
var tbody = row.parentNode,
inspectorContainer = this.getInspectorContainer(),
selectedRow = this.getSelectedRow()
if (selectedRow === row && !forceSelect) {
return
}
if (selectedRow) {
if (!this.validateKeyValue()) {
return
}
if (this.currentRowInspector) {
if (!this.currentRowInspector.validate()) {
return
}
}
this.applyDataToRow(selectedRow)
$.wn.foundation.element.removeClass(selectedRow, 'active')
}
this.disposeInspector()
$.wn.foundation.element.addClass(row, 'active')
this.createInspectorForRow(row, inspectorContainer)
}
ObjectListEditor.prototype.createInspectorForRow = function(row, inspectorContainer) {
var dataStr = row.getAttribute('data-inspector-values')
if (dataStr === undefined || typeof dataStr !== 'string') {
throw new Error('Values not found for the selected row.')
}
var properties = this.propertyDefinition.itemProperties,
values = JSON.parse(dataStr),
options = {
enableExternalParameterEditor: false,
onChange: this.proxy(this.onInspectorDataChange),
inspectorClass: this.inspector.options.inspectorClass,
parentContainer: this.getRootSurface(),
}
this.currentRowInspector = new $.wn.inspector.surface(inspectorContainer, properties, values,
$.wn.inspector.helpers.generateElementUniqueId(inspectorContainer), options, null, null, this.propertyDefinition.property)
}
ObjectListEditor.prototype.disposeInspector = function() {
$.wn.foundation.controlUtils.disposeControls(this.popup.querySelector('[data-inspector-container]'))
this.currentRowInspector = null
}
ObjectListEditor.prototype.applyDataToRow = function(row) {
if (this.currentRowInspector === null) {
return
}
var data = this.currentRowInspector.getValues()
row.setAttribute('data-inspector-values', JSON.stringify(data))
}
ObjectListEditor.prototype.updateRowText = function(property, value) {
var selectedRow = this.getSelectedRow()
if (!selectedRow) {
throw new Exception('A row is not found for the updated data')
}
if (property !== this.propertyDefinition.titleProperty) {
return
}
value = $.trim(value)
if (value.length === 0) {
value = '[No title]'
$.wn.foundation.element.addClass(selectedRow, 'disabled')
}
else {
$.wn.foundation.element.removeClass(selectedRow, 'disabled')
}
selectedRow.firstChild.textContent = value
}
ObjectListEditor.prototype.getSelectedRow = function() {
if (!this.popup) {
throw new Error('Trying to get selected row without a popup reference.')
}
var rows = this.getTableBody().children
for (var i = 0, len = rows.length; i < len; i++) {
if ($.wn.foundation.element.hasClass(rows[i], 'active')) {
return rows[i]
}
}
return null
}
ObjectListEditor.prototype.createItem = function() {
var selectedRow = this.getSelectedRow()
if (selectedRow) {
if (!this.validateKeyValue()) {
return
}
if (this.currentRowInspector) {
if (!this.currentRowInspector.validate()) {
return
}
}
this.applyDataToRow(selectedRow)
$.wn.foundation.element.removeClass(selectedRow, 'active')
}
this.disposeInspector()
var title = 'New item',
row = this.buildTableRow(title, 'rowlink active'),
tbody = this.getTableBody(),
data = {}
data[this.propertyDefinition.titleProperty] = title
row.setAttribute('data-inspector-values', JSON.stringify(data))
tbody.appendChild(row)
this.selectRow(row, true)
this.removeEmptyRow()
this.updateScrollpads()
}
ObjectListEditor.prototype.deleteItem = function() {
var selectedRow = this.getSelectedRow()
if (!selectedRow) {
return
}
var nextRow = selectedRow.nextElementSibling,
prevRow = selectedRow.previousElementSibling,
tbody = this.getTableBody()
this.disposeInspector()
tbody.removeChild(selectedRow)
var newSelectedRow = nextRow ? nextRow : prevRow
if (newSelectedRow) {
this.selectRow(newSelectedRow)
}
else {
tbody.appendChild(this.buildEmptyRow())
}
this.updateScrollpads()
}
ObjectListEditor.prototype.applyDataToParentInspector = function() {
var selectedRow = this.getSelectedRow(),
tbody = this.getTableBody(),
dataRows = tbody.querySelectorAll('tr[data-inspector-values]'),
link = this.getLink(),
result = this.getEmptyValue()
if (selectedRow) {
if (!this.validateKeyValue()) {
return
}
if (this.currentRowInspector) {
if (!this.currentRowInspector.validate()) {
return
}
}
this.applyDataToRow(selectedRow)
}
for (var i = 0, len = dataRows.length; i < len; i++) {
var dataRow = dataRows[i],
rowData = JSON.parse(dataRow.getAttribute('data-inspector-values'))
if (!this.isKeyValueMode()) {
result.push(rowData)
}
else {
var rowKey = rowData[this.propertyDefinition.keyProperty]
result[rowKey] = this.removeKeyProperty(rowData)
}
}
this.inspector.setPropertyValue(this.propertyDefinition.property, result)
this.setLinkText(link, result)
$(link).popup('hide')
return false
}
ObjectListEditor.prototype.validateKeyValue = function() {
if (!this.isKeyValueMode()) {
return true
}
if (this.currentRowInspector === null) {
return true
}
var data = this.currentRowInspector.getValues(),
keyProperty = this.propertyDefinition.keyProperty
if (data[keyProperty] === undefined) {
throw new Error('Key property ' + keyProperty + ' is not found in the Inspector data. Property: ' + this.propertyDefinition.property)
}
var keyPropertyValue = data[keyProperty],
keyPropertyTitle = this.getKeyProperty().title
if (typeof keyPropertyValue !== 'string') {
throw new Error('Key property (' + keyProperty + ') value should be a string. Property: ' + this.propertyDefinition.property)
}
if ($.trim(keyPropertyValue).length === 0) {
$.wn.flashMsg({text: 'The value of key property ' + keyPropertyTitle + ' cannot be empty.', 'class': 'error', 'interval': 3})
return false
}
var selectedRow = this.getSelectedRow(),
tbody = this.getTableBody(),
dataRows = tbody.querySelectorAll('tr[data-inspector-values]')
for (var i = 0, len = dataRows.length; i < len; i++) {
var dataRow = dataRows[i],
rowData = JSON.parse(dataRow.getAttribute('data-inspector-values'))
if (selectedRow == dataRow) {
continue
}
if (rowData[keyProperty] == keyPropertyValue) {
$.wn.flashMsg({text: 'The value of key property ' + keyPropertyTitle + ' should be unique.', 'class': 'error', 'interval': 3})
return false
}
}
return true
}
//
// Helpers
//
ObjectListEditor.prototype.getLink = function() {
return this.containerCell.querySelector('a.trigger')
}
ObjectListEditor.prototype.getPopupFormElement = function() {
var form = this.popup.querySelector('form')
if (!form) {
this.throwError('Cannot find form element in the popup window.')
}
return form
}
ObjectListEditor.prototype.getInspectorContainer = function() {
return this.popup.querySelector('div[data-inspector-container]')
}
ObjectListEditor.prototype.getTableBody = function() {
return this.popup.querySelector('table.inspector-table-list tbody')
}
ObjectListEditor.prototype.isKeyValueMode = function() {
return this.propertyDefinition.keyProperty !== undefined
}
ObjectListEditor.prototype.getKeyProperty = function() {
for (var i = 0, len = this.propertyDefinition.itemProperties.length; i < len; i++) {
var property = this.propertyDefinition.itemProperties[i]
if (property.property == this.propertyDefinition.keyProperty) {
return property
}
}
}
ObjectListEditor.prototype.getValueKeys = function(value) {
var result = []
for (var key in value) {
result.push(key)
}
return result
}
ObjectListEditor.prototype.addKeyProperty = function(key, value) {
if (!this.isKeyValueMode()) {
return value
}
value[this.propertyDefinition.keyProperty] = key
return value
}
ObjectListEditor.prototype.removeKeyProperty = function(value) {
if (!this.isKeyValueMode()) {
return value
}
var result = value
if (result[this.propertyDefinition.keyProperty] !== undefined) {
delete result[this.propertyDefinition.keyProperty]
}
return result
}
ObjectListEditor.prototype.getEmptyValue = function() {
if (this.isKeyValueMode()) {
return {}
}
else {
return []
}
}
//
// Event handlers
//
ObjectListEditor.prototype.registerHandlers = function() {
var link = this.getLink(),
$link = $(link)
link.addEventListener('click', this.proxy(this.onTriggerClick))
$link.on('shown.oc.popup', this.proxy(this.onPopupShown))
$link.on('hidden.oc.popup', this.proxy(this.onPopupHidden))
}
ObjectListEditor.prototype.unregisterHandlers = function() {
var link = this.getLink(),
$link = $(link)
link.removeEventListener('click', this.proxy(this.onTriggerClick))
$link.off('shown.oc.popup', this.proxy(this.onPopupShown))
$link.off('hidden.oc.popup', this.proxy(this.onPopupHidden))
}
ObjectListEditor.prototype.onTriggerClick = function(ev) {
$.wn.foundation.event.stop(ev)
var content = this.getPopupContent()
content = content.replace('{{property}}', this.propertyDefinition.title)
$(ev.target).popup({
content: content
})
return false
}
ObjectListEditor.prototype.onPopupShown = function(ev, link, popup) {
$(popup).on('submit.inspector', 'form', this.proxy(this.onSubmit))
$(popup).on('click', 'tr.rowlink', this.proxy(this.onRowClick))
$(popup).on('click.inspector', '[data-cmd]', this.proxy(this.onCommand))
this.popup = popup.get(0)
this.buildPopupContents(this.popup)
this.getRootSurface().popupDisplayed()
}
ObjectListEditor.prototype.onPopupHidden = function(ev, link, popup) {
$(popup).off('.inspector', this.proxy(this.onSubmit))
$(popup).off('click', 'tr.rowlink', this.proxy(this.onRowClick))
$(popup).off('click.inspector', '[data-cmd]', this.proxy(this.onCommand))
this.disposeInspector()
$.wn.foundation.controlUtils.disposeControls(this.popup)
this.popup = null
this.getRootSurface().popupHidden()
}
ObjectListEditor.prototype.onSubmit = function(ev) {
this.applyDataToParentInspector()
ev.preventDefault()
return false
}
ObjectListEditor.prototype.onRowClick = function(ev) {
this.selectRow(ev.currentTarget)
}
ObjectListEditor.prototype.onInspectorDataChange = function(property, value) {
this.updateRowText(property, value)
}
ObjectListEditor.prototype.onCommand = function(ev) {
var command = ev.currentTarget.getAttribute('data-cmd')
switch (command) {
case 'create-item' :
this.createItem()
break;
case 'delete-item' :
this.deleteItem()
break;
}
}
//
// Disposing
//
ObjectListEditor.prototype.removeControls = function() {
if (this.popup) {
this.disposeInspector(this.popup)
}
}
$.wn.inspector.propertyEditors.objectList = ObjectListEditor
}(window.jQuery);

View File

@@ -0,0 +1,137 @@
/*
* Base class for Inspector editors that create popups.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.propertyEditors.base,
BaseProto = Base.prototype
var PopupBase = function(inspector, propertyDefinition, containerCell, group) {
this.popup = null
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
PopupBase.prototype = Object.create(BaseProto)
PopupBase.prototype.constructor = Base
PopupBase.prototype.dispose = function() {
this.unregisterHandlers()
this.popup = null
BaseProto.dispose.call(this)
}
PopupBase.prototype.build = function() {
var link = document.createElement('a')
$.wn.foundation.element.addClass(link, 'trigger')
link.setAttribute('href', '#')
this.setLinkText(link)
$.wn.foundation.element.addClass(this.containerCell, 'trigger-cell')
this.containerCell.appendChild(link)
}
PopupBase.prototype.setLinkText = function(link, value) {
}
PopupBase.prototype.getPopupContent = function() {
return '<form> \
<div class="modal-header"> \
<button type="button" class="close" data-dismiss="popup">&times;</button> \
<h4 class="modal-title">{{property}}</h4> \
</div> \
<div class="modal-body"> \
<div class="form-group"> \
</div> \
</div> \
<div class="modal-footer"> \
<button type="submit" class="btn btn-primary">OK</button> \
<button type="button" class="btn btn-default" data-dismiss="popup">Cancel</button> \
</div> \
</form>'
}
PopupBase.prototype.updateDisplayedValue = function(value) {
this.setLinkText(this.getLink(), value)
}
PopupBase.prototype.registerHandlers = function() {
var link = this.getLink(),
$link = $(link)
link.addEventListener('click', this.proxy(this.onTriggerClick))
$link.on('shown.oc.popup', this.proxy(this.onPopupShown))
$link.on('hidden.oc.popup', this.proxy(this.onPopupHidden))
}
PopupBase.prototype.unregisterHandlers = function() {
var link = this.getLink(),
$link = $(link)
link.removeEventListener('click', this.proxy(this.onTriggerClick))
$link.off('shown.oc.popup', this.proxy(this.onPopupShown))
$link.off('hidden.oc.popup', this.proxy(this.onPopupHidden))
}
PopupBase.prototype.getLink = function() {
return this.containerCell.querySelector('a.trigger')
}
PopupBase.prototype.configurePopup = function(popup) {
}
PopupBase.prototype.handleSubmit = function($form) {
}
PopupBase.prototype.hidePopup = function() {
$(this.getLink()).popup('hide')
}
PopupBase.prototype.onTriggerClick = function(ev) {
$.wn.foundation.event.stop(ev)
var content = this.getPopupContent()
content = content.replace('{{property}}', this.propertyDefinition.title)
$(ev.target).popup({
content: content
})
return false
}
PopupBase.prototype.onPopupShown = function(ev, link, popup) {
$(popup).on('submit.inspector', 'form', this.proxy(this.onSubmit))
this.popup = popup.get(0)
this.configurePopup(popup)
this.getRootSurface().popupDisplayed()
}
PopupBase.prototype.onPopupHidden = function(ev, link, popup) {
$(popup).off('.inspector', 'form', this.proxy(this.onSubmit))
this.popup = null
this.getRootSurface().popupHidden()
}
PopupBase.prototype.onSubmit = function(ev) {
ev.preventDefault()
if (this.handleSubmit($(ev.target)) === false) {
return false
}
this.setLinkText(this.getLink())
this.hidePopup()
return false
}
$.wn.inspector.propertyEditors.popupBase = PopupBase
}(window.jQuery);

View File

@@ -0,0 +1,375 @@
/*
* Inspector set editor class.
*
* This class uses $.wn.inspector.propertyEditors.checkbox editor.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.propertyEditors.base,
BaseProto = Base.prototype
var SetEditor = function(inspector, propertyDefinition, containerCell, group) {
this.editors = []
this.loadedItems = null
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
SetEditor.prototype = Object.create(BaseProto)
SetEditor.prototype.constructor = Base
SetEditor.prototype.init = function() {
this.initControlGroup()
BaseProto.init.call(this)
}
SetEditor.prototype.dispose = function() {
this.disposeEditors()
this.disposeControls()
this.editors = null
BaseProto.dispose.call(this)
}
//
// Building
//
SetEditor.prototype.build = function() {
var link = document.createElement('a')
$.wn.foundation.element.addClass(link, 'trigger')
link.setAttribute('href', '#')
this.setLinkText(link)
$.wn.foundation.element.addClass(this.containerCell, 'trigger-cell')
this.containerCell.appendChild(link)
if (this.propertyDefinition.items !== undefined) {
this.loadStaticItems()
}
else {
this.loadDynamicItems()
}
}
SetEditor.prototype.loadStaticItems = function() {
var itemArray = []
for (var itemValue in this.propertyDefinition.items) {
itemArray.push({
value: itemValue,
title: this.propertyDefinition.items[itemValue]
})
}
for (var i = itemArray.length-1; i >=0; i--) {
this.buildItemEditor(String(itemArray[i].value), itemArray[i].title)
}
}
SetEditor.prototype.setLinkText = function(link, value) {
var value = (value !== undefined && value !== null) ? value
: this.getNormalizedValue(),
text = '[ ]'
if (value === undefined) {
value = this.propertyDefinition.default
}
if (value !== undefined && value.length !== undefined && value.length > 0 && typeof value !== 'string') {
var textValues = []
for (var i = 0, len = value.length; i < len; i++) {
textValues.push(this.valueToText(value[i]))
}
text = '[' + textValues.join(', ') + ']'
$.wn.foundation.element.removeClass(link, 'placeholder')
}
else {
text = this.propertyDefinition.placeholder
if ((typeof text === 'string' && text.length == 0) || text === undefined) {
text = '[ ]'
}
$.wn.foundation.element.addClass(link, 'placeholder')
}
link.textContent = text
}
SetEditor.prototype.buildItemEditor = function(value, text) {
var property = {
title: text,
itemType: 'property',
groupIndex: this.group.getGroupIndex()
},
newRow = this.createGroupedRow(property),
currentRow = this.containerCell.parentNode,
tbody = this.containerCell.parentNode.parentNode, // row / tbody
cell = document.createElement('td')
this.buildCheckbox(cell, value, text)
newRow.appendChild(cell)
tbody.insertBefore(newRow, currentRow.nextSibling)
}
SetEditor.prototype.buildCheckbox = function(cell, value, title) {
var property = {
property: value,
title: title,
default: this.isCheckedByDefault(value)
},
editor = new $.wn.inspector.propertyEditors.checkbox(this, property, cell, this.group)
this.editors.push(editor)
}
SetEditor.prototype.isCheckedByDefault = function(value) {
if (!this.propertyDefinition.default) {
return false
}
return this.propertyDefinition.default.indexOf(value) > -1
}
//
// Dynamic items
//
SetEditor.prototype.showLoadingIndicator = function() {
$(this.getLink()).loadIndicator()
}
SetEditor.prototype.hideLoadingIndicator = function() {
if (this.isDisposed()) {
return
}
var $link = $(this.getLink())
$link.loadIndicator('hide')
$link.loadIndicator('destroy')
}
SetEditor.prototype.loadDynamicItems = function() {
var link = this.getLink(),
data = this.inspector.getValues(),
$form = $(link).closest('form')
$.wn.foundation.element.addClass(link, 'loading-indicator-container size-small')
this.showLoadingIndicator()
data.inspectorProperty = this.getPropertyPath()
data.inspectorClassName = this.inspector.options.inspectorClass
$form.request('onInspectableGetOptions', {
data: data,
})
.done(this.proxy(this.itemsRequestDone))
.always(this.proxy(this.hideLoadingIndicator))
}
SetEditor.prototype.itemsRequestDone = function(data, currentValue, initialization) {
if (this.isDisposed()) {
// Handle the case when the asynchronous request finishes after
// the editor is disposed
return
}
this.loadedItems = {}
if (data.options) {
for (var i = data.options.length-1; i >= 0; i--) {
this.buildItemEditor(data.options[i].value, data.options[i].title)
this.loadedItems[String(data.options[i].value)] = data.options[i].title
}
}
this.setLinkText(this.getLink())
}
//
// Helpers
//
SetEditor.prototype.getLink = function() {
return this.containerCell.querySelector('a.trigger')
}
SetEditor.prototype.getItemsSource = function() {
if (this.propertyDefinition.items !== undefined) {
return this.propertyDefinition.items
}
return this.loadedItems
}
SetEditor.prototype.valueToText = function(value) {
var source = this.getItemsSource()
if (!source) {
return value
}
for (var itemValue in source) {
if (itemValue == value) {
return source[itemValue]
}
}
return value
}
/*
* Removes items that don't exist in the defined items from
* the value.
*/
SetEditor.prototype.cleanUpValue = function(value) {
if (!value) {
return value
}
var result = [],
source = this.getItemsSource()
for (var i = 0, len = value.length; i < len; i++) {
var currentValue = value[i]
if (source[currentValue] !== undefined) {
result.push(currentValue)
}
}
return result
}
SetEditor.prototype.getNormalizedValue = function() {
var value = this.inspector.getPropertyValue(this.propertyDefinition.property)
if (value === null) {
value = undefined
}
if (value === undefined) {
return value
}
if (value.length === undefined || typeof value === 'string') {
return undefined
}
return value
}
//
// Editor API methods
//
SetEditor.prototype.supportsExternalParameterEditor = function() {
return false
}
SetEditor.prototype.isGroupedEditor = function() {
return true
}
//
// Inspector API methods
//
// This editor creates checkbox editor and acts as a container Inspector
// for them. The methods in this section emulate and proxy some functionality
// of the Inspector.
//
SetEditor.prototype.getPropertyValue = function(checkboxValue) {
// When a checkbox requests the property value, we return
// TRUE if the checkbox value is listed in the current values of
// the set.
// For example, the available set items are [create, update], the
// current set value is [create] and checkboxValue is "create".
// The result of the method will be TRUE.
var value = this.getNormalizedValue(),
checkboxValueStr = String(checkboxValue)
if (value === undefined) {
return this.isCheckedByDefault(checkboxValueStr)
}
if (!value) {
return false
}
return value.indexOf(checkboxValueStr) > -1
}
SetEditor.prototype.setPropertyValue = function(checkboxValue, isChecked) {
// In this method the Set Editor mimics the Surface.
// It acts as a parent surface for the children checkboxes,
// watching changes in them and updating the link text.
var currentValue = this.getNormalizedValue(),
checkboxValueStr = String(checkboxValue)
if (currentValue === undefined) {
currentValue = this.propertyDefinition.default
}
if (!currentValue) {
currentValue = []
}
var resultValue = [],
items = this.getItemsSource()
for (var itemValue in items) {
if (itemValue !== checkboxValueStr) {
if (currentValue.indexOf(itemValue) !== -1) {
resultValue.push(itemValue)
}
}
else {
if (isChecked) {
resultValue.push(itemValue)
}
}
}
this.inspector.setPropertyValue(this.propertyDefinition.property, this.cleanUpValue(resultValue))
this.setLinkText(this.getLink())
}
SetEditor.prototype.generateSequencedId = function() {
return this.inspector.generateSequencedId()
}
//
// Disposing
//
SetEditor.prototype.disposeEditors = function() {
for (var i = 0, len = this.editors.length; i < len; i++) {
var editor = this.editors[i]
editor.dispose()
}
}
SetEditor.prototype.disposeControls = function() {
var link = this.getLink()
if (this.propertyDefinition.items === undefined) {
$(link).loadIndicator('destroy')
}
link.parentNode.removeChild(link)
}
$.wn.inspector.propertyEditors.set = SetEditor
}(window.jQuery);

View File

@@ -0,0 +1,88 @@
/*
* Inspector string editor class.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.propertyEditors.base,
BaseProto = Base.prototype
var StringEditor = function(inspector, propertyDefinition, containerCell, group) {
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
StringEditor.prototype = Object.create(BaseProto)
StringEditor.prototype.constructor = Base
StringEditor.prototype.dispose = function() {
this.unregisterHandlers()
BaseProto.dispose.call(this)
}
StringEditor.prototype.build = function() {
var editor = document.createElement('input'),
placeholder = this.propertyDefinition.placeholder !== undefined ? this.propertyDefinition.placeholder : '',
value = this.inspector.getPropertyValue(this.propertyDefinition.property)
editor.setAttribute('type', 'text')
editor.setAttribute('class', 'string-editor')
editor.setAttribute('placeholder', placeholder)
if (value === undefined) {
value = this.propertyDefinition.default
}
if (value === undefined) {
value = ''
}
editor.value = value
$.wn.foundation.element.addClass(this.containerCell, 'text')
this.containerCell.appendChild(editor)
}
StringEditor.prototype.updateDisplayedValue = function(value) {
this.getInput().value = value
}
StringEditor.prototype.getInput = function() {
return this.containerCell.querySelector('input')
}
StringEditor.prototype.focus = function() {
this.getInput().focus()
this.onInputFocus()
}
StringEditor.prototype.registerHandlers = function() {
var input = this.getInput()
input.addEventListener('focus', this.proxy(this.onInputFocus))
input.addEventListener('keyup', this.proxy(this.onInputKeyUp))
}
StringEditor.prototype.unregisterHandlers = function() {
var input = this.getInput()
input.removeEventListener('focus', this.proxy(this.onInputFocus))
input.removeEventListener('keyup', this.proxy(this.onInputKeyUp))
}
StringEditor.prototype.onInputFocus = function(ev) {
this.inspector.makeCellActive(this.containerCell)
}
StringEditor.prototype.onInputKeyUp = function() {
var value = $.trim(this.getInput().value)
this.inspector.setPropertyValue(this.propertyDefinition.property, value)
}
StringEditor.prototype.onExternalPropertyEditorHidden = function() {
this.focus()
}
$.wn.inspector.propertyEditors.string = StringEditor
}(window.jQuery);

View File

@@ -0,0 +1,96 @@
/*
* Inspector string list editor class.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.propertyEditors.text,
BaseProto = Base.prototype
var StringListEditor = function(inspector, propertyDefinition, containerCell, group) {
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
StringListEditor.prototype = Object.create(BaseProto)
StringListEditor.prototype.constructor = Base
StringListEditor.prototype.setLinkText = function(link, value) {
var value = value !== undefined ? value
: this.inspector.getPropertyValue(this.propertyDefinition.property)
if (value === undefined) {
value = this.propertyDefinition.default
}
this.checkValueType(value)
if (!value) {
value = this.propertyDefinition.placeholder
$.wn.foundation.element.addClass(link, 'placeholder')
if (!value) {
value = '[]'
}
link.textContent = value
}
else {
$.wn.foundation.element.removeClass(link, 'placeholder')
link.textContent = '[' + value.join(', ') + ']'
}
}
StringListEditor.prototype.checkValueType = function(value) {
if (value && Object.prototype.toString.call(value) !== '[object Array]') {
this.throwError('The string list value should be an array.')
}
}
StringListEditor.prototype.configurePopup = function(popup) {
var $textarea = $(popup).find('textarea'),
value = this.inspector.getPropertyValue(this.propertyDefinition.property)
if (this.propertyDefinition.placeholder) {
$textarea.attr('placeholder', this.propertyDefinition.placeholder)
}
if (value === undefined) {
value = this.propertyDefinition.default
}
this.checkValueType(value)
if (value && value.length) {
$textarea.val(value.join('\n'))
}
$textarea.focus()
this.configureComment(popup)
}
StringListEditor.prototype.handleSubmit = function($form) {
var $textarea = $form.find('textarea'),
link = this.getLink(),
value = $.trim($textarea.val()),
arrayValue = [],
resultValue = []
if (value.length) {
value = value.replace(/\r\n/g, '\n')
arrayValue = value.split('\n')
for (var i = 0, len = arrayValue.length; i < len; i++) {
var currentValue = $.trim(arrayValue[i])
if (currentValue.length > 0) {
resultValue.push(currentValue)
}
}
}
this.inspector.setPropertyValue(this.propertyDefinition.property, resultValue)
}
$.wn.inspector.propertyEditors.stringList = StringListEditor
}(window.jQuery);

View File

@@ -0,0 +1,549 @@
/*
* Inspector string list with autocompletion editor class.
*
* TODO: validation is not implemented in this editor. See the Dictionary editor for reference.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.propertyEditors.popupBase,
BaseProto = Base.prototype
var StringListAutocomplete = function(inspector, propertyDefinition, containerCell, group) {
this.items = null
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
StringListAutocomplete.prototype = Object.create(BaseProto)
StringListAutocomplete.prototype.constructor = Base
StringListAutocomplete.prototype.dispose = function() {
BaseProto.dispose.call(this)
}
StringListAutocomplete.prototype.init = function() {
BaseProto.init.call(this)
}
StringListAutocomplete.prototype.supportsExternalParameterEditor = function() {
return false
}
StringListAutocomplete.prototype.setLinkText = function(link, value) {
var value = value !== undefined ? value
: this.inspector.getPropertyValue(this.propertyDefinition.property)
if (value === undefined) {
value = this.propertyDefinition.default
}
this.checkValueType(value)
if (!value) {
value = this.propertyDefinition.placeholder
$.wn.foundation.element.addClass(link, 'placeholder')
if (!value) {
value = '[]'
}
link.textContent = value
}
else {
$.wn.foundation.element.removeClass(link, 'placeholder')
link.textContent = '[' + value.join(', ') + ']'
}
}
StringListAutocomplete.prototype.checkValueType = function(value) {
if (value && Object.prototype.toString.call(value) !== '[object Array]') {
this.throwError('The string list value should be an array.')
}
}
//
// Popup editor methods
//
StringListAutocomplete.prototype.getPopupContent = function() {
return '<form> \
<div class="modal-header"> \
<button type="button" class="close" data-dismiss="popup">&times;</button> \
<h4 class="modal-title">{{property}}</h4> \
</div> \
<div class="modal-body"> \
<div class="control-toolbar"> \
<div class="toolbar-item"> \
<div class="btn-group"> \
<button type="button" class="btn btn-primary \
wn-icon-plus" \
data-cmd="create-item">Add</button> \
<button type="button" class="btn btn-default \
empty wn-icon-trash-o" \
data-cmd="delete-item"></button> \
</div> \
</div> \
</div> \
<div class="form-group"> \
<div class="inspector-dictionary-container"> \
<div class="values"> \
<div class="control-scrollpad" \
data-control="scrollpad"> \
<div class="scroll-wrapper"> \
<table class=" \
no-offset-bottom \
inspector-dictionary-table"> \
</table> \
</div> \
</div> \
</div> \
</div> \
</div> \
</div> \
<div class="modal-footer"> \
<button type="submit" class="btn btn-primary">OK</button> \
<button type="button" class="btn btn-default" data-dismiss="popup">Cancel</button> \
</div> \
</form>'
}
StringListAutocomplete.prototype.configurePopup = function(popup) {
this.initAutocomplete()
this.buildItemsTable(popup.get(0))
this.focusFirstInput()
}
StringListAutocomplete.prototype.handleSubmit = function($form) {
return this.applyValues()
}
//
// Building and row management
//
StringListAutocomplete.prototype.buildItemsTable = function(popup) {
var table = popup.querySelector('table.inspector-dictionary-table'),
tbody = document.createElement('tbody'),
items = this.inspector.getPropertyValue(this.propertyDefinition.property)
if (items === undefined) {
items = this.propertyDefinition.default
}
if (items === undefined || this.getValueKeys(items).length === 0) {
var row = this.buildEmptyRow()
tbody.appendChild(row)
}
else {
for (var key in items) {
var row = this.buildTableRow(items[key])
tbody.appendChild(row)
}
}
table.appendChild(tbody)
this.updateScrollpads()
}
StringListAutocomplete.prototype.buildTableRow = function(value) {
var row = document.createElement('tr'),
valueCell = document.createElement('td')
this.createInput(valueCell, value)
row.appendChild(valueCell)
return row
}
StringListAutocomplete.prototype.buildEmptyRow = function() {
return this.buildTableRow(null)
}
StringListAutocomplete.prototype.createInput = function(container, value) {
var input = document.createElement('input'),
controlContainer = document.createElement('div')
input.setAttribute('type', 'text')
input.setAttribute('class', 'form-control')
input.value = value
controlContainer.appendChild(input)
container.appendChild(controlContainer)
}
StringListAutocomplete.prototype.setActiveCell = function(input) {
var activeCells = this.popup.querySelectorAll('td.active')
for (var i = activeCells.length-1; i >= 0; i--) {
$.wn.foundation.element.removeClass(activeCells[i], 'active')
}
var activeCell = input.parentNode.parentNode // input / div / td
$.wn.foundation.element.addClass(activeCell, 'active')
this.buildAutoComplete(input)
}
StringListAutocomplete.prototype.createItem = function() {
var activeRow = this.getActiveRow(),
newRow = this.buildEmptyRow(),
tbody = this.getTableBody(),
nextSibling = activeRow ? activeRow.nextElementSibling : null
tbody.insertBefore(newRow, nextSibling)
this.focusAndMakeActive(newRow.querySelector('input'))
this.updateScrollpads()
}
StringListAutocomplete.prototype.deleteItem = function() {
var activeRow = this.getActiveRow(),
tbody = this.getTableBody()
if (!activeRow) {
return
}
var nextRow = activeRow.nextElementSibling,
prevRow = activeRow.previousElementSibling,
input = this.getRowInputByIndex(activeRow, 0)
if (input) {
this.removeAutocomplete(input)
}
tbody.removeChild(activeRow)
var newSelectedRow = nextRow ? nextRow : prevRow
if (!newSelectedRow) {
newSelectedRow = this.buildEmptyRow()
tbody.appendChild(newSelectedRow)
}
this.focusAndMakeActive(newSelectedRow.querySelector('input'))
this.updateScrollpads()
}
StringListAutocomplete.prototype.applyValues = function() {
var tbody = this.getTableBody(),
dataRows = tbody.querySelectorAll('tr'),
link = this.getLink(),
result = []
for (var i = 0, len = dataRows.length; i < len; i++) {
var dataRow = dataRows[i],
valueInput = this.getRowInputByIndex(dataRow, 0),
value = $.trim(valueInput.value)
if (value.length == 0) {
continue
}
result.push(value)
}
this.inspector.setPropertyValue(this.propertyDefinition.property, result)
this.setLinkText(link, result)
}
//
// Helpers
//
StringListAutocomplete.prototype.getValueKeys = function(value) {
var result = []
for (var key in value) {
result.push(key)
}
return result
}
StringListAutocomplete.prototype.getActiveRow = function() {
var activeCell = this.popup.querySelector('td.active')
if (!activeCell) {
return null
}
return activeCell.parentNode
}
StringListAutocomplete.prototype.getTableBody = function() {
return this.popup.querySelector('table.inspector-dictionary-table tbody')
}
StringListAutocomplete.prototype.updateScrollpads = function() {
$('.control-scrollpad', this.popup).scrollpad('update')
}
StringListAutocomplete.prototype.focusFirstInput = function() {
var input = this.popup.querySelector('td input')
if (input) {
input.focus()
this.setActiveCell(input)
}
}
StringListAutocomplete.prototype.getEditorCell = function(cell) {
return cell.parentNode.parentNode // cell / div / td
}
StringListAutocomplete.prototype.getEditorRow = function(cell) {
return cell.parentNode.parentNode.parentNode // cell / div / td / tr
}
StringListAutocomplete.prototype.focusAndMakeActive = function(input) {
input.focus()
this.setActiveCell(input)
}
StringListAutocomplete.prototype.getRowInputByIndex = function(row, index) {
return row.cells[index].querySelector('input')
}
//
// Navigation
//
StringListAutocomplete.prototype.navigateDown = function(ev) {
var cell = this.getEditorCell(ev.currentTarget),
row = this.getEditorRow(ev.currentTarget),
nextRow = row.nextElementSibling
if (!nextRow) {
return
}
var newActiveEditor = nextRow.cells[cell.cellIndex].querySelector('input')
this.focusAndMakeActive(newActiveEditor)
}
StringListAutocomplete.prototype.navigateUp = function(ev) {
var cell = this.getEditorCell(ev.currentTarget),
row = this.getEditorRow(ev.currentTarget),
prevRow = row.previousElementSibling
if (!prevRow) {
return
}
var newActiveEditor = prevRow.cells[cell.cellIndex].querySelector('input')
this.focusAndMakeActive(newActiveEditor)
}
//
// Autocomplete
//
StringListAutocomplete.prototype.initAutocomplete = function() {
if (this.propertyDefinition.items !== undefined) {
this.items = this.prepareItems(this.propertyDefinition.items)
this.initializeAutocompleteForCurrentInput()
}
else {
this.loadDynamicItems()
}
}
StringListAutocomplete.prototype.initializeAutocompleteForCurrentInput = function() {
var activeElement = document.activeElement
if (!activeElement) {
return
}
var inputs = this.popup.querySelectorAll('td input.form-control')
if (!inputs) {
return
}
for (var i=inputs.length-1; i>=0; i--) {
if (inputs[i] === activeElement) {
this.buildAutoComplete(inputs[i])
return
}
}
}
StringListAutocomplete.prototype.buildAutoComplete = function(input) {
if (this.items === null) {
return
}
$(input).autocomplete({
source: this.items,
matchWidth: true,
menu: '<ul class="autocomplete dropdown-menu inspector-autocomplete"></ul>',
bodyContainer: true
})
}
StringListAutocomplete.prototype.removeAutocomplete = function(input) {
var $input = $(input)
if (!$input.data('autocomplete')) {
return
}
$input.autocomplete('destroy')
}
StringListAutocomplete.prototype.prepareItems = function(items) {
var result = {}
if ($.isArray(items)) {
for (var i = 0, len = items.length; i < len; i++) {
result[items[i]] = items[i]
}
}
else {
result = items
}
return result
}
StringListAutocomplete.prototype.loadDynamicItems = function() {
if (this.isDisposed()) {
return
}
var data = this.getRootSurface().getValues(),
$form = $(this.popup).find('form')
if (this.triggerGetItems(data) === false) {
return
}
data['inspectorProperty'] = this.getPropertyPath()
data['inspectorClassName'] = this.inspector.options.inspectorClass
$form.request('onInspectableGetOptions', {
data: data,
})
.done(this.proxy(this.itemsRequestDone))
}
StringListAutocomplete.prototype.triggerGetItems = function(values) {
var $inspectable = this.getInspectableElement()
if (!$inspectable) {
return true
}
var itemsEvent = $.Event('autocompleteitems.oc.inspector')
$inspectable.trigger(itemsEvent, [{
values: values,
callback: this.proxy(this.itemsRequestDone),
property: this.inspector.getPropertyPath(this.propertyDefinition.property),
propertyDefinition: this.propertyDefinition
}])
if (itemsEvent.isDefaultPrevented()) {
return false
}
return true
}
StringListAutocomplete.prototype.itemsRequestDone = function(data) {
if (this.isDisposed()) {
// Handle the case when the asynchronous request finishes after
// the editor is disposed
return
}
var loadedItems = {}
if (data.options) {
for (var i = data.options.length-1; i >= 0; i--) {
loadedItems[data.options[i].value] = data.options[i].title
}
}
this.items = this.prepareItems(loadedItems)
this.initializeAutocompleteForCurrentInput()
}
StringListAutocomplete.prototype.removeAutocompleteFromAllRows = function() {
var inputs = this.popup.querySelector('td input.form-control')
for (var i=inputs.length-1; i>=0; i--) {
this.removeAutocomplete(inputs[i])
}
}
//
// Event handlers
//
StringListAutocomplete.prototype.onPopupShown = function(ev, link, popup) {
BaseProto.onPopupShown.call(this,ev, link, popup)
popup.on('focus.inspector', 'td input', this.proxy(this.onFocus))
popup.on('blur.inspector', 'td input', this.proxy(this.onBlur))
popup.on('keydown.inspector', 'td input', this.proxy(this.onKeyDown))
popup.on('click.inspector', '[data-cmd]', this.proxy(this.onCommand))
}
StringListAutocomplete.prototype.onPopupHidden = function(ev, link, popup) {
popup.off('.inspector', 'td input')
popup.off('.inspector', '[data-cmd]', this.proxy(this.onCommand))
this.removeAutocompleteFromAllRows()
this.items = null
BaseProto.onPopupHidden.call(this, ev, link, popup)
}
StringListAutocomplete.prototype.onFocus = function(ev) {
this.setActiveCell(ev.currentTarget)
}
StringListAutocomplete.prototype.onBlur = function(ev) {
if ($(ev.relatedTarget).closest('ul.inspector-autocomplete').length > 0) {
// Do not close the autocomplete results if a drop-down
// menu item was clicked
return
}
this.removeAutocomplete(ev.currentTarget)
}
StringListAutocomplete.prototype.onCommand = function(ev) {
var command = ev.currentTarget.getAttribute('data-cmd')
switch (command) {
case 'create-item' :
this.createItem()
break;
case 'delete-item' :
this.deleteItem()
break;
}
}
StringListAutocomplete.prototype.onKeyDown = function(ev) {
if (ev.key === 'ArrowDown') {
return this.navigateDown(ev)
}
else if (ev.key === 'ArrowUp') {
return this.navigateUp(ev)
}
}
$.wn.inspector.propertyEditors.stringListAutocomplete = StringListAutocomplete
}(window.jQuery);

View File

@@ -0,0 +1,97 @@
/*
* Inspector text editor class.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.propertyEditors.popupBase,
BaseProto = Base.prototype
var TextEditor = function(inspector, propertyDefinition, containerCell, group) {
Base.call(this, inspector, propertyDefinition, containerCell, group)
}
TextEditor.prototype = Object.create(BaseProto)
TextEditor.prototype.constructor = Base
TextEditor.prototype.setLinkText = function(link, value) {
var value = value !== undefined ? value
: this.inspector.getPropertyValue(this.propertyDefinition.property)
if (value === undefined) {
value = this.propertyDefinition.default
}
if (!value) {
value = this.propertyDefinition.placeholder
$.wn.foundation.element.addClass(link, 'placeholder')
}
else {
$.wn.foundation.element.removeClass(link, 'placeholder')
}
if (typeof value === 'string') {
value = value.replace(/(?:\r\n|\r|\n)/g, ' ');
value = $.trim(value)
value = value.substring(0, 300);
}
link.textContent = value
}
TextEditor.prototype.getPopupContent = function() {
return '<form> \
<div class="modal-header"> \
<button type="button" class="close" data-dismiss="popup">&times;</button> \
<h4 class="modal-title">{{property}}</h4> \
</div> \
<div class="modal-body"> \
<div class="form-group"> \
<p class="inspector-field-comment"></p> \
<textarea class="form-control size-small field-textarea" name="name"> \
</textarea> \
</div> \
</div> \
<div class="modal-footer"> \
<button type="submit" class="btn btn-primary">OK</button> \
<button type="button" class="btn btn-default" data-dismiss="popup">Cancel</button> \
</div> \
</form>'
}
TextEditor.prototype.configureComment = function(popup) {
if (!this.propertyDefinition.description) {
return
}
var descriptionElement = $(popup).find('p.inspector-field-comment')
descriptionElement.text(this.propertyDefinition.description)
}
TextEditor.prototype.configurePopup = function(popup) {
var $textarea = $(popup).find('textarea'),
value = this.inspector.getPropertyValue(this.propertyDefinition.property)
if (this.propertyDefinition.placeholder) {
$textarea.attr('placeholder', this.propertyDefinition.placeholder)
}
if (value === undefined) {
value = this.propertyDefinition.default
}
$textarea.val(value)
$textarea.focus()
this.configureComment(popup)
}
TextEditor.prototype.handleSubmit = function($form) {
var $textarea = $form.find('textarea'),
link = this.getLink(),
value = $.trim($textarea.val())
this.inspector.setPropertyValue(this.propertyDefinition.property, value)
}
$.wn.inspector.propertyEditors.text = TextEditor
}(window.jQuery);

View File

@@ -0,0 +1,103 @@
/*
* Inspector engine helpers.
*
* The helpers are used mostly by the Inspector Surface.
*
*/
+function ($) { "use strict";
// NAMESPACES
// ============================
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
if ($.wn.inspector === undefined)
$.wn.inspector = {}
$.wn.inspector.engine = {}
// CLASS DEFINITION
// ============================
function findGroup(group, properties) {
for (var i = 0, len = properties.length; i < len; i++) {
var property = properties[i]
if (property.itemType !== undefined && property.itemType == 'group' && property.title == group) {
return property
}
}
return null
}
$.wn.inspector.engine.processPropertyGroups = function(properties) {
var fields = [],
result = {
hasGroups: false,
properties: []
},
groupIndex = 0
for (var i = 0, len = properties.length; i < len; i++) {
var property = properties[i]
if (property['sortOrder'] === undefined) {
property['sortOrder'] = (i+1)*20
}
}
properties.sort(function(a, b){
return a['sortOrder'] - b['sortOrder']
})
for (var i = 0, len = properties.length; i < len; i++) {
var property = properties[i]
property.itemType = 'property'
if (property.group === undefined) {
fields.push(property)
}
else {
var group = findGroup(property.group, fields)
if (!group) {
group = {
itemType: 'group',
title: property.group,
properties: [],
groupIndex: groupIndex
}
groupIndex++
fields.push(group)
}
property.groupIndex = group.groupIndex
group.properties.push(property)
}
}
for (var i = 0, len = fields.length; i < len; i++) {
var property = fields[i]
result.properties.push(property)
if (property.itemType == 'group') {
result.hasGroups = true
for (var j = 0, propertiesLen = property.properties.length; j < propertiesLen; j++) {
result.properties.push(property.properties[j])
}
delete property.properties
}
}
return result
}
}(window.jQuery);

View File

@@ -0,0 +1,336 @@
/*
* External parameter editor for Inspector.
*
* The external parameter editor allows to use URL and
* other external parameters as values for the inspectable
* properties.
*
*/
+function ($) { "use strict";
// NAMESPACES
// ============================
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
if ($.wn.inspector === undefined)
$.wn.inspector = {}
// CLASS DEFINITION
// ============================
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var ExternalParameterEditor = function(inspector, propertyDefinition, containerCell, initialValue) {
this.inspector = inspector
this.propertyDefinition = propertyDefinition
this.containerCell = containerCell
this.initialValue = initialValue
Base.call(this)
this.init()
}
ExternalParameterEditor.prototype = Object.create(BaseProto)
ExternalParameterEditor.prototype.constructor = Base
ExternalParameterEditor.prototype.dispose = function() {
this.disposeControls()
this.unregisterHandlers()
this.inspector = null
this.propertyDefinition = null
this.containerCell = null
this.initialValue = null
BaseProto.dispose.call(this)
}
ExternalParameterEditor.prototype.init = function() {
this.tooltipText = 'Click to enter the external parameter name to load the property value from'
this.build()
this.registerHandlers()
this.setInitialValue()
}
/**
* Builds the external parameter editor markup:
*
* <div class="external-param-editor-container">
* <input> <-- original property editing input/markup
* <div class="external-editor">
* <div class="controls">
* <input type="text" tabindex="-1"/>
* <a href="#" tabindex="-1">
* <i class="wn-icon-terminal"></i>
* </a>
* </div>
* </div>
* </div>
*/
ExternalParameterEditor.prototype.build = function() {
var container = document.createElement('div'),
editor = document.createElement('div'),
controls = document.createElement('div'),
input = document.createElement('input'),
link = document.createElement('a'),
icon = document.createElement('i')
container.setAttribute('class', 'external-param-editor-container')
editor.setAttribute('class', 'external-editor')
controls.setAttribute('class', 'controls')
input.setAttribute('type', 'text')
input.setAttribute('tabindex', '-1')
link.setAttribute('href', '#')
link.setAttribute('class', 'external-editor-link')
link.setAttribute('tabindex', '-1')
link.setAttribute('title', this.tooltipText)
$(link).tooltip({'container': 'body', delay: 500})
icon.setAttribute('class', 'wn-icon-terminal')
link.appendChild(icon)
controls.appendChild(input)
controls.appendChild(link)
editor.appendChild(controls)
while (this.containerCell.firstChild) {
var child = this.containerCell.firstChild
container.appendChild(child)
}
container.appendChild(editor)
this.containerCell.appendChild(container)
}
ExternalParameterEditor.prototype.setInitialValue = function() {
if (!this.initialValue) {
return
}
if (typeof this.initialValue !== 'string') {
return
}
var matches = []
if (matches = this.initialValue.match(/^\{\{([^\}]+)\}\}$/)) {
var value = $.trim(matches[1])
if (value.length > 0) {
this.showEditor(true)
this.getInput().value = value
this.inspector.setPropertyValue(this.propertyDefinition.property, null, true, true)
}
}
}
ExternalParameterEditor.prototype.showEditor = function(building) {
var editor = this.getEditor(),
input = this.getInput(),
container = this.getContainer(),
link = this.getLink()
var position = $(editor).position()
if (!building) {
editor.style.right = 0
editor.style.left = position.left + 'px'
}
else {
editor.style.right = 0
}
setTimeout(this.proxy(this.repositionEditor), 0)
$.wn.foundation.element.addClass(container, 'editor-visible')
link.setAttribute('data-original-title', 'Click to enter the property value')
this.toggleEditorVisibility(false)
input.setAttribute('tabindex', 0)
if (!building) {
input.focus()
}
}
ExternalParameterEditor.prototype.repositionEditor = function() {
this.getEditor().style.left = 0
this.containerCell.scrollTop = 0
}
ExternalParameterEditor.prototype.hideEditor = function() {
var editor = this.getEditor(),
container = this.getContainer()
editor.style.left = 'auto'
editor.style.right = '30px'
$.wn.foundation.element.removeClass(container, 'editor-visible')
$.wn.foundation.element.removeClass(this.containerCell, 'active')
var propertyEditor = this.inspector.findPropertyEditor(this.propertyDefinition.property)
if (propertyEditor) {
propertyEditor.onExternalPropertyEditorHidden()
}
}
ExternalParameterEditor.prototype.toggleEditor = function(ev) {
$.wn.foundation.event.stop(ev)
var link = this.getLink(),
container = this.getContainer(),
editor = this.getEditor()
$(link).tooltip('hide')
if (!this.isEditorVisible()) {
this.showEditor()
return
}
var left = container.offsetWidth
editor.style.left = left + 'px'
link.setAttribute('data-original-title', this.tooltipText)
this.getInput().setAttribute('tabindex', '-1')
this.toggleEditorVisibility(true)
setTimeout(this.proxy(this.hideEditor), 200)
}
ExternalParameterEditor.prototype.toggleEditorVisibility = function(show) {
var container = this.getContainer(),
children = container.children,
height = 0
if (!show) {
height = this.containerCell.getAttribute('data-inspector-cell-height')
if (!height) {
height = $(this.containerCell).height()
this.containerCell.setAttribute('data-inspector-cell-height', height)
}
}
// Fixed value instead of trying to get the container cell height.
// If the editor is contained in initially hidden editor (collapsed group),
// the container cell will be unknown.
height = Math.max(height, 19)
for (var i = 0, len = children.length; i < len; i++) {
var element = children[i]
if ($.wn.foundation.element.hasClass(element, 'external-editor')) {
continue
}
if (show) {
$.wn.foundation.element.removeClass(element, 'hide')
}
else {
container.style.height = height + 'px'
$.wn.foundation.element.addClass(element, 'hide')
}
}
}
ExternalParameterEditor.prototype.focus = function() {
this.getInput().focus()
}
ExternalParameterEditor.prototype.validate = function(silentMode) {
var value = $.trim(this.getValue())
if (value.length === 0) {
if (!silentMode) {
$.wn.flashMsg({text: 'Please enter the external parameter name.', 'class': 'error', 'interval': 5})
this.focus()
}
return false
}
return true
}
//
// Event handlers
//
ExternalParameterEditor.prototype.registerHandlers = function() {
var input = this.getInput()
this.getLink().addEventListener('click', this.proxy(this.toggleEditor))
input.addEventListener('focus', this.proxy(this.onInputFocus))
input.addEventListener('change', this.proxy(this.onInputChange))
}
ExternalParameterEditor.prototype.onInputFocus = function() {
this.inspector.makeCellActive(this.containerCell)
}
ExternalParameterEditor.prototype.onInputChange = function() {
this.inspector.markPropertyChanged(this.propertyDefinition.property, true)
}
//
// Disposing
//
ExternalParameterEditor.prototype.unregisterHandlers = function() {
var input = this.getInput()
this.getLink().removeEventListener('click', this.proxy(this.toggleEditor))
input.removeEventListener('focus', this.proxy(this.onInputFocus))
input.removeEventListener('change', this.proxy(this.onInputChange))
}
ExternalParameterEditor.prototype.disposeControls = function() {
$(this.getLink()).tooltip('destroy')
}
//
// Helpers
//
ExternalParameterEditor.prototype.getInput = function() {
return this.containerCell.querySelector('div.external-editor input')
}
ExternalParameterEditor.prototype.getValue = function() {
return this.getInput().value
}
ExternalParameterEditor.prototype.getLink = function() {
return this.containerCell.querySelector('a.external-editor-link')
}
ExternalParameterEditor.prototype.getContainer = function() {
return this.containerCell.querySelector('div.external-param-editor-container')
}
ExternalParameterEditor.prototype.getEditor = function() {
return this.containerCell.querySelector('div.external-editor')
}
ExternalParameterEditor.prototype.getPropertyName = function() {
return this.propertyDefinition.property
}
ExternalParameterEditor.prototype.isEditorVisible = function() {
return $.wn.foundation.element.hasClass(this.getContainer(), 'editor-visible')
}
$.wn.inspector.externalParameterEditor = ExternalParameterEditor
}(window.jQuery);

View File

@@ -0,0 +1,268 @@
/*
* Inspector grouping support.
*
*/
+function ($) { "use strict";
// GROUP MANAGER CLASS
// ============================
var GroupManager = function(controlId) {
this.controlId = controlId
this.rootGroup = null
this.cachedGroupStatuses = null
}
GroupManager.prototype.createGroup = function(groupId, parentGroup) {
var group = new Group(groupId)
if (parentGroup) {
parentGroup.groups.push(group)
group.parentGroup = parentGroup // Circular reference, but memory leaks are not noticed
}
else {
this.rootGroup = group
}
return group
}
GroupManager.prototype.getGroupIndex = function(group) {
return group.getGroupIndex()
}
GroupManager.prototype.isParentGroupExpanded = function(group) {
if (!group.parentGroup) {
// The root group is always expanded
return true
}
return this.isGroupExpanded(group.parentGroup)
}
GroupManager.prototype.isGroupExpanded = function(group) {
if (!group.parentGroup) {
// The root group is always expanded
return true
}
var groupIndex = this.getGroupIndex(group),
statuses = this.readGroupStatuses()
if (statuses[groupIndex] !== undefined) {
return statuses[groupIndex]
}
return false
}
GroupManager.prototype.setGroupStatus = function(groupIndex, expanded) {
var statuses = this.readGroupStatuses()
statuses[groupIndex] = expanded
this.writeGroupStatuses(statuses)
}
GroupManager.prototype.readGroupStatuses = function() {
if (this.cachedGroupStatuses !== null) {
return this.cachedGroupStatuses
}
var statuses = getInspectorGroupStatuses()
if (statuses[this.controlId] !== undefined) {
this.cachedGroupStatuses = statuses[this.controlId]
}
else {
this.cachedGroupStatuses = {}
}
return this.cachedGroupStatuses
}
GroupManager.prototype.writeGroupStatuses = function(updatedStatuses) {
var statuses = getInspectorGroupStatuses()
statuses[this.controlId] = updatedStatuses
setInspectorGroupStatuses(statuses)
this.cachedGroupStatuses = updatedStatuses
}
GroupManager.prototype.findGroupByIndex = function(index) {
return this.rootGroup.findGroupByIndex(index)
}
GroupManager.prototype.findGroupRows = function(table, index, ignoreCollapsedSubgroups) {
var group = this.findGroupByIndex(index)
if (!group) {
throw new Error('Cannot find the requested row group.')
}
return group.findGroupRows(table, ignoreCollapsedSubgroups, this)
}
GroupManager.prototype.markGroupRowInvalid = function(group, table) {
var currentGroup = group
while (currentGroup) {
var row = currentGroup.findGroupRow(table)
if (row) {
$.wn.foundation.element.addClass(row, 'invalid')
}
currentGroup = currentGroup.parentGroup
}
}
GroupManager.prototype.unmarkInvalidGroups = function(table) {
var rows = table.querySelectorAll('tr.invalid')
for (var i = rows.length-1; i >= 0; i--) {
$.wn.foundation.element.removeClass(rows[i], 'invalid')
}
}
GroupManager.prototype.isRowVisible = function(table, rowGroupIndex) {
var group = this.findGroupByIndex(index)
if (!group) {
throw new Error('Cannot find the requested row group.')
}
var current = group
while (current) {
if (!this.isGroupExpanded(current)) {
return false
}
current = current.parentGroup
}
return true
}
//
// Internal functions
//
function getInspectorGroupStatuses() {
var statuses = document.body.getAttribute('data-inspector-group-statuses')
if (statuses !== null) {
return JSON.parse(statuses)
}
return {}
}
function setInspectorGroupStatuses(statuses) {
document.body.setAttribute('data-inspector-group-statuses', JSON.stringify(statuses))
}
// GROUP CLASS
// ============================
var Group = function(groupId) {
this.groupId = groupId
this.parentGroup = null
this.groupIndex = null
this.groups = []
}
Group.prototype.getGroupIndex = function() {
if (this.groupIndex !== null) {
return this.groupIndex
}
var result = '',
current = this
while (current) {
if (result.length > 0) {
result = current.groupId + '-' + result
}
else {
result = String(current.groupId)
}
current = current.parentGroup
}
this.groupIndex = result
return result
}
Group.prototype.findGroupByIndex = function(index) {
if (this.getGroupIndex() == index) {
return this
}
for (var i = this.groups.length-1; i >= 0; i--) {
var groupResult = this.groups[i].findGroupByIndex(index)
if (groupResult !== null) {
return groupResult
}
}
return null
}
Group.prototype.getLevel = function() {
var current = this,
level = -1
while (current) {
level++
current = current.parentGroup
}
return level
}
Group.prototype.getGroupAndAllParents = function() {
var current = this,
result = []
while (current) {
result.push(current)
current = current.parentGroup
}
return result
}
Group.prototype.findGroupRows = function(table, ignoreCollapsedSubgroups, groupManager) {
var groupIndex = this.getGroupIndex(),
rows = table.querySelectorAll('tr[data-parent-group-index="'+groupIndex+'"]'),
result = Array.prototype.slice.call(rows) // Convert node list to array
for (var i = 0, len = this.groups.length; i < len; i++) {
var subgroup = this.groups[i]
if (ignoreCollapsedSubgroups && !groupManager.isGroupExpanded(subgroup)) {
continue
}
var subgroupRows = subgroup.findGroupRows(table, ignoreCollapsedSubgroups, groupManager)
for (var j = 0, subgroupLen = subgroupRows.length; j < subgroupLen; j++) {
result.push(subgroupRows[j])
}
}
return result
}
Group.prototype.findGroupRow = function(table) {
return table.querySelector('tr[data-group-index="'+this.groupIndex+'"]')
}
$.wn.inspector.groupManager = GroupManager
}(window.jQuery);

View File

@@ -0,0 +1,35 @@
/*
* Inspector helper functions.
*
*/
+function ($) { "use strict";
// NAMESPACES
// ============================
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
if ($.wn.inspector === undefined)
$.wn.inspector = {}
$.wn.inspector.helpers = {}
$.wn.inspector.helpers.generateElementUniqueId = function(element) {
if (element.hasAttribute('data-inspector-id')) {
return element.getAttribute('data-inspector-id')
}
var id = $.wn.inspector.helpers.generateUniqueId()
element.setAttribute('data-inspector-id', id)
return id
}
$.wn.inspector.helpers.generateUniqueId = function() {
return "inspectorid-" + Math.floor(Math.random() * new Date().getTime());
}
}(window.jQuery)

View File

@@ -0,0 +1,178 @@
/*
* Inspector management functions.
*
* Watches inspectable elements clicks and creates Inspector surfaces in popups
* and containers.
*/
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var InspectorManager = function() {
Base.call(this)
this.init()
}
InspectorManager.prototype = Object.create(BaseProto)
InspectorManager.prototype.constructor = Base
InspectorManager.prototype.init = function() {
$(document).on('click', '[data-inspectable]', this.proxy(this.onInspectableClicked))
}
InspectorManager.prototype.getContainerElement = function($element) {
var $containerHolder = $element.closest('[data-inspector-container]')
if ($containerHolder.length === 0) {
return null
}
var $container = $containerHolder.find($containerHolder.data('inspector-container'))
if ($container.length === 0) {
throw new Error('Inspector container ' + $containerHolder.data['inspector-container'] + ' element is not found.')
}
return $container
}
InspectorManager.prototype.loadElementOptions = function($element) {
var options = {}
// Only specific options are allowed, don't load all options with data()
//
if ($element.data('inspector-css-class')) {
options.inspectorCssClass = $element.data('inspector-css-class')
}
return options
}
InspectorManager.prototype.createInspectorPopup = function($element, containerSupported) {
var options = $.extend(this.loadElementOptions($element), {
containerSupported: containerSupported
})
new $.wn.inspector.wrappers.popup($element, null, options)
}
InspectorManager.prototype.createInspectorContainer = function($element, $container) {
var options = $.extend(this.loadElementOptions($element), {
containerSupported: true,
container: $container
})
new $.wn.inspector.wrappers.container($element, null, options)
}
InspectorManager.prototype.switchToPopup = function(wrapper) {
var options = $.extend(this.loadElementOptions(wrapper.$element), {
containerSupported: true
})
new $.wn.inspector.wrappers.popup(wrapper.$element, wrapper, options)
wrapper.cleanupAfterSwitch()
this.setContainerPreference(false)
}
InspectorManager.prototype.switchToContainer = function(wrapper) {
var $container = this.getContainerElement(wrapper.$element),
options = $.extend(this.loadElementOptions(wrapper.$element), {
containerSupported: true,
container: $container
})
if (!$container) {
throw new Error('Cannot switch to container: a container element is not found')
}
new $.wn.inspector.wrappers.container(wrapper.$element, wrapper, options)
wrapper.cleanupAfterSwitch()
this.setContainerPreference(true)
}
InspectorManager.prototype.createInspector = function(element) {
var $element = $(element)
if ($element.data('oc.inspectorVisible')) {
return false
}
var $container = this.getContainerElement($element)
// If there's no container option, create the Inspector popup
//
if (!$container) {
this.createInspectorPopup($element, false)
}
else {
// If the container is already in use, apply values to the inspectable elements
if (!this.applyValuesFromContainer($container) || !this.containerHidingAllowed($container)) {
return
}
// Dispose existing container wrapper, if any
$.wn.foundation.controlUtils.disposeControls($container.get(0))
if (!this.getContainerPreference()) {
// If container is not a preferred option, create Inspector popoup
this.createInspectorPopup($element, true)
}
else {
// Otherwise, create Inspector in the container
this.createInspectorContainer($element, $container)
}
}
}
InspectorManager.prototype.getContainerPreference = function() {
if (!Modernizr.localstorage) {
return false
}
return localStorage.getItem('oc.inspectorUseContainer') === "true"
}
InspectorManager.prototype.setContainerPreference = function(value) {
if (!Modernizr.localstorage) {
return
}
return localStorage.setItem('oc.inspectorUseContainer', value ? "true" : "false")
}
InspectorManager.prototype.applyValuesFromContainer = function($container) {
var applyEvent = $.Event('apply.oc.inspector')
$container.trigger(applyEvent)
return !applyEvent.isDefaultPrevented();
}
InspectorManager.prototype.containerHidingAllowed = function($container) {
var allowedEvent = $.Event('beforeContainerHide.oc.inspector')
$container.trigger(allowedEvent)
return !allowedEvent.isDefaultPrevented();
}
InspectorManager.prototype.onInspectableClicked = function(ev) {
var $element = $(ev.currentTarget)
if (this.createInspector($element) === false) {
return false
}
ev.stopPropagation()
return false
}
$.wn.inspector.manager = new InspectorManager()
$.fn.inspector = function () {
return this.each(function () {
$.wn.inspector.manager.createInspector(this)
})
}
}(window.jQuery);

View File

@@ -0,0 +1,975 @@
/*
* Inspector Surface class.
*
* The class creates Inspector user interface and all the editors
* corresponding to the passed configuration in a specified container
* element.
*
*/
+function ($) { "use strict";
// NAMESPACES
// ============================
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
if ($.wn.inspector === undefined)
$.wn.inspector = {}
// CLASS DEFINITION
// ============================
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
/**
* Creates the Inspector surface in a container.
* - containerElement container DOM element
* - properties array (array of objects)
* - values - property values, an object
* - inspectorUniqueId - a string containing the unique inspector identifier.
* The identifier should be a constant for an inspectable element. Use
* $.wn.inspector.helpers.generateElementUniqueId(element) to generate a persistent ID
* for an element. Use $.wn.inspector.helpers.generateUniqueId() to generate an ID
* not associated with an element. Inspector uses the ID for storing configuration
* related to an element in the document DOM.
*/
var Surface = function(containerElement, properties, values, inspectorUniqueId, options, parentSurface, group, propertyName) {
if (inspectorUniqueId === undefined) {
throw new Error('Inspector surface unique ID should be defined.')
}
this.options = $.extend({}, Surface.DEFAULTS, typeof options == 'object' && options)
this.rawProperties = properties
this.parsedProperties = $.wn.inspector.engine.processPropertyGroups(properties)
this.container = containerElement
this.inspectorUniqueId = inspectorUniqueId
this.values = values !== null ? values : {}
this.originalValues = $.extend(true, {}, this.values) // Clone the values hash
this.idCounter = 1
this.popupCounter = 0
this.parentSurface = parentSurface
this.propertyName = propertyName
this.editors = []
this.externalParameterEditors = []
this.tableContainer = null
this.groupManager = null
this.group = null
if (group !== undefined) {
this.group = group
}
if (!this.parentSurface) {
this.groupManager = new $.wn.inspector.groupManager(this.inspectorUniqueId)
}
Base.call(this)
this.init()
}
Surface.prototype = Object.create(BaseProto)
Surface.prototype.constructor = Surface
Surface.prototype.dispose = function() {
this.unregisterHandlers()
this.disposeControls()
this.disposeEditors()
this.removeElements()
this.disposeExternalParameterEditors()
this.container = null
this.tableContainer = null
this.rawProperties = null
this.parsedProperties = null
this.editors = null
this.externalParameterEditors = null
this.values = null
this.originalValues = null
this.options.onChange = null
this.options.onPopupDisplayed = null
this.options.onPopupHidden = null
this.options.onGetInspectableElement = null
this.parentSurface = null
this.groupManager = null
this.group = null
BaseProto.dispose.call(this)
}
// INTERNAL METHODS
// ============================
Surface.prototype.init = function() {
if (this.groupManager && !this.group) {
this.group = this.groupManager.createGroup('root')
}
this.build()
if (!this.parentSurface) {
$.wn.foundation.controlUtils.markDisposable(this.tableContainer)
}
this.registerHandlers()
}
Surface.prototype.registerHandlers = function() {
if (!this.parentSurface) {
$(this.tableContainer).one('dispose-control', this.proxy(this.dispose))
$(this.tableContainer).on('click', 'tr.group, tr.control-group', this.proxy(this.onGroupClick))
$(this.tableContainer).on('focus-control', this.proxy(this.focusFirstEditor))
}
}
Surface.prototype.unregisterHandlers = function() {
if (!this.parentSurface) {
$(this.tableContainer).off('dispose-control', this.proxy(this.dispose))
$(this.tableContainer).off('click', 'tr.group, tr.control-group', this.proxy(this.onGroupClick))
$(this.tableContainer).off('focus-control', this.proxy(this.focusFirstEditor))
}
}
//
// Building
//
/**
* Builds the Inspector table. The markup generated by this method looks
* like this:
*
* <div>
* <table>
* <tbody>
* <tr>
* <th data-property="label">
* <div>
* <div>
* <span class="title-element" title="Label">
* <a href="javascript:;" class="expandControl expanded" data-group-index="1">Expand/Collapse</a>
* Label
* </span>
* </div>
* </div>
* </th>
* <td>
* Editor markup
* </td>
* </tr>
* </tbody>
* </table>
* </div>
*/
Surface.prototype.build = function() {
this.tableContainer = document.createElement('div')
var dataTable = document.createElement('table'),
tbody = document.createElement('tbody')
$.wn.foundation.element.addClass(dataTable, 'inspector-fields')
if (this.parsedProperties.hasGroups) {
$.wn.foundation.element.addClass(dataTable, 'has-groups')
}
var currentGroup = this.group
for (var i=0, len = this.parsedProperties.properties.length; i < len; i++) {
var property = this.parsedProperties.properties[i]
if (property.itemType == 'group') {
currentGroup = this.getGroupManager().createGroup(property.groupIndex, this.group)
}
else {
if (property.groupIndex === undefined) {
currentGroup = this.group
}
}
var row = this.buildRow(property, currentGroup)
if (property.itemType == 'group') {
this.applyGroupLevelToRow(row, currentGroup.parentGroup)
}
else {
this.applyGroupLevelToRow(row, currentGroup)
}
tbody.appendChild(row)
// Editor
//
this.buildEditor(row, property, dataTable, currentGroup)
}
dataTable.appendChild(tbody)
this.tableContainer.appendChild(dataTable)
this.container.appendChild(this.tableContainer)
if (this.options.enableExternalParameterEditor) {
this.buildExternalParameterEditor(tbody)
}
if (!this.parentSurface) {
this.focusFirstEditor()
}
}
Surface.prototype.moveToContainer = function(newContainer) {
this.container = newContainer
this.container.appendChild(this.tableContainer)
}
Surface.prototype.buildRow = function(property, group) {
var row = document.createElement('tr'),
th = document.createElement('th'),
titleSpan = document.createElement('span'),
description = this.buildPropertyDescription(property)
// Table row
//
if (property.property) {
row.setAttribute('data-property', property.property)
row.setAttribute('data-property-path', this.getPropertyPath(property.property))
}
this.applyGroupIndexAttribute(property, row, group)
$.wn.foundation.element.addClass(row, this.getRowCssClass(property, group))
// Property head
//
this.applyHeadColspan(th, property)
titleSpan.setAttribute('class', 'title-element')
titleSpan.setAttribute('title', this.escapeJavascriptString(property.title))
this.buildGroupExpandControl(titleSpan, property, false, false, group)
titleSpan.innerHTML += this.escapeJavascriptString(property.title)
var outerDiv = document.createElement('div'),
innerDiv = document.createElement('div')
innerDiv.appendChild(titleSpan)
if (description) {
innerDiv.appendChild(description)
}
outerDiv.appendChild(innerDiv)
th.appendChild(outerDiv)
row.appendChild(th)
return row
}
Surface.prototype.focusFirstEditor = function() {
if (this.editors.length == 0) {
return
}
var groupManager = this.getGroupManager()
for (var i = 0, len = this.editors.length; i < len; i++) {
var editor = this.editors[i],
group = editor.parentGroup
if (group && !this.groupManager.isGroupExpanded(group) ) {
continue
}
var externalParameterEditor = this.findExternalParameterEditor(editor.getPropertyName())
if (externalParameterEditor && externalParameterEditor.isEditorVisible()) {
externalParameterEditor.focus()
return
}
editor.focus()
return
}
}
Surface.prototype.getRowCssClass = function(property, group) {
var result = property.itemType
if (property.itemType == 'property') {
// result += ' grouped'
if (group.parentGroup) {
result += this.getGroupManager().isGroupExpanded(group) ? ' expanded' : ' collapsed'
}
}
if (property.itemType == 'property' && !property.showExternalParam) {
result += ' no-external-parameter'
}
return result
}
Surface.prototype.applyHeadColspan = function(th, property) {
if (property.itemType == 'group') {
th.setAttribute('colspan', 2)
}
}
Surface.prototype.buildGroupExpandControl = function(titleSpan, property, force, hasChildSurface, group) {
if (property.itemType !== 'group' && !force) {
return
}
var groupIndex = this.getGroupManager().getGroupIndex(group),
statusClass = this.getGroupManager().isGroupExpanded(group) ? 'expanded' : '',
anchor = document.createElement('a')
anchor.setAttribute('class', 'expandControl ' + statusClass)
anchor.setAttribute('href', 'javascript:;')
anchor.innerHTML = '<span>Expand/collapse</span>'
titleSpan.appendChild(anchor)
}
Surface.prototype.buildPropertyDescription = function(property) {
if (property.description === undefined || property.description === null) {
return null
}
var span = document.createElement('span')
span.setAttribute('title', this.escapeJavascriptString(property.description))
span.setAttribute('class', 'info wn-icon-info with-tooltip')
$(span).tooltip({ placement: 'auto right', container: 'body', delay: 500 })
return span
}
Surface.prototype.buildExternalParameterEditor = function(tbody) {
var rows = tbody.children
for (var i = 0, len = rows.length; i < len; i++) {
var row = rows[i],
property = row.getAttribute('data-property')
if ($.wn.foundation.element.hasClass(row, 'no-external-parameter') || !property) {
continue
}
var propertyEditor = this.findPropertyEditor(property)
if (propertyEditor && !propertyEditor.supportsExternalParameterEditor()) {
continue
}
var cell = row.querySelector('td'),
propertyDefinition = this.findPropertyDefinition(property),
initialValue = this.getPropertyValue(property)
if (initialValue === undefined) {
initialValue = propertyEditor.getUndefinedValue()
}
var editor = new $.wn.inspector.externalParameterEditor(this, propertyDefinition, cell, initialValue)
this.externalParameterEditors.push(editor)
}
}
//
// Field grouping
//
Surface.prototype.applyGroupIndexAttribute = function(property, row, group, isGroupedControl) {
if (property.itemType == 'group' || isGroupedControl) {
row.setAttribute('data-group-index', this.getGroupManager().getGroupIndex(group))
row.setAttribute('data-parent-group-index', this.getGroupManager().getGroupIndex(group.parentGroup))
}
else {
if (group.parentGroup) {
row.setAttribute('data-parent-group-index', this.getGroupManager().getGroupIndex(group))
}
}
}
Surface.prototype.applyGroupLevelToRow = function(row, group) {
if (row.hasAttribute('data-group-level')) {
return
}
var th = this.getRowHeadElement(row)
if (th === null) {
throw new Error('Cannot find TH element for the Inspector row')
}
var groupLevel = group.getLevel()
row.setAttribute('data-group-level', groupLevel)
th.children[0].style.marginLeft = groupLevel*10 + 'px'
}
Surface.prototype.toggleGroup = function(row, forceExpand) {
var link = row.querySelector('a'),
groupIndex = row.getAttribute('data-group-index'),
table = this.getRootTable(),
groupManager = this.getGroupManager(),
collapse = true
if ($.wn.foundation.element.hasClass(link, 'expanded') && !forceExpand) {
$.wn.foundation.element.removeClass(link, 'expanded')
}
else {
$.wn.foundation.element.addClass(link, 'expanded')
collapse = false
}
var propertyRows = groupManager.findGroupRows(table, groupIndex, !collapse),
duration = Math.round(50 / propertyRows.length)
this.expandOrCollapseRows(propertyRows, collapse, duration, forceExpand)
groupManager.setGroupStatus(groupIndex, !collapse)
}
Surface.prototype.expandGroupParents = function(group) {
var groups = group.getGroupAndAllParents(),
table = this.getRootTable()
for (var i = groups.length-1; i >= 0; i--) {
var row = groups[i].findGroupRow(table)
if (row) {
this.toggleGroup(row, true)
}
}
}
Surface.prototype.expandOrCollapseRows = function(rows, collapse, duration, noAnimation) {
var row = rows.pop(),
self = this
if (row) {
if (!noAnimation) {
setTimeout(function toggleRow() {
$.wn.foundation.element.toggleClass(row, 'collapsed', collapse)
$.wn.foundation.element.toggleClass(row, 'expanded', !collapse)
self.expandOrCollapseRows(rows, collapse, duration, noAnimation)
}, duration)
}
else {
$.wn.foundation.element.toggleClass(row, 'collapsed', collapse)
$.wn.foundation.element.toggleClass(row, 'expanded', !collapse)
self.expandOrCollapseRows(rows, collapse, duration, noAnimation)
}
}
}
Surface.prototype.getGroupManager = function() {
return this.getRootSurface().groupManager
}
//
// Editors
//
Surface.prototype.buildEditor = function(row, property, dataTable, group) {
if (property.itemType !== 'property') {
return
}
this.validateEditorType(property.type)
var cell = document.createElement('td'),
type = property.type
row.appendChild(cell)
if (type === undefined) {
type = 'string'
}
var editor = new $.wn.inspector.propertyEditors[type](this, property, cell, group)
if (editor.isGroupedEditor()) {
$.wn.foundation.element.addClass(dataTable, 'has-groups')
$.wn.foundation.element.addClass(row, 'control-group')
this.applyGroupIndexAttribute(property, row, editor.group, true)
this.buildGroupExpandControl(row.querySelector('span.title-element'), property, true, editor.hasChildSurface(), editor.group)
if (cell.children.length == 0) {
// If the editor hasn't added any elements to the cell,
// and it's a grouped control, remove the cell and
// make the group title full-width.
row.querySelector('th').setAttribute('colspan', 2)
row.removeChild(cell)
}
}
this.editors.push(editor)
}
Surface.prototype.generateSequencedId = function() {
this.idCounter ++
return this.inspectorUniqueId + '-' + this.idCounter
}
//
// Internal API for the editors
//
Surface.prototype.getPropertyValue = function(property) {
return this.values[property]
}
Surface.prototype.setPropertyValue = function(property, value, supressChangeEvents, forceEditorUpdate) {
if (value !== undefined) {
this.values[property] = value
}
else {
if (this.values[property] !== undefined) {
delete this.values[property]
}
}
if (!supressChangeEvents) {
if (this.originalValues[property] === undefined || !this.comparePropertyValues(this.originalValues[property], value)) {
this.markPropertyChanged(property, true)
}
else {
this.markPropertyChanged(property, false)
}
var propertyPath = this.getPropertyPath(property)
this.getRootSurface().notifyEditorsPropertyChanged(propertyPath, value)
if (this.options.onChange !== null) {
this.options.onChange(property, value)
}
}
if (forceEditorUpdate) {
var editor = this.findPropertyEditor(property)
if (editor) {
editor.updateDisplayedValue(value)
}
}
return value
}
Surface.prototype.notifyEditorsPropertyChanged = function(propertyPath, value) {
// Editors use this event to watch changes in properties
// they depend on. All editors should be notified, including
// editors in nested surfaces. The property name is passed as a
// path object.property (if the property is nested), so that
// property depenencies could be defined as
// ['property', 'object.property']
for (var i = 0, len = this.editors.length; i < len; i++) {
var editor = this.editors[i]
editor.onInspectorPropertyChanged(propertyPath, value)
editor.notifyChildSurfacesPropertyChanged(propertyPath, value)
}
}
Surface.prototype.makeCellActive = function(cell) {
var tbody = cell.parentNode.parentNode.parentNode, // cell / row / tbody
cells = tbody.querySelectorAll('tr td')
for (var i = 0, len = cells.length; i < len; i++) {
$.wn.foundation.element.removeClass(cells[i], 'active')
}
$.wn.foundation.element.addClass(cell, 'active')
}
Surface.prototype.markPropertyChanged = function(property, changed) {
var propertyPath = this.getPropertyPath(property),
row = this.tableContainer.querySelector('tr[data-property-path="'+propertyPath+'"]')
if (changed) {
$.wn.foundation.element.addClass(row, 'changed')
}
else {
$.wn.foundation.element.removeClass(row, 'changed')
}
}
Surface.prototype.findPropertyEditor = function(property) {
for (var i = 0, len = this.editors.length; i < len; i++) {
if (this.editors[i].getPropertyName() == property) {
return this.editors[i]
}
}
return null
}
Surface.prototype.findExternalParameterEditor = function(property) {
for (var i = 0, len = this.externalParameterEditors.length; i < len; i++) {
if (this.externalParameterEditors[i].getPropertyName() == property) {
return this.externalParameterEditors[i]
}
}
return null
}
Surface.prototype.findPropertyDefinition = function(property) {
for (var i=0, len = this.parsedProperties.properties.length; i < len; i++) {
var definition = this.parsedProperties.properties[i]
if (definition.property == property) {
return definition
}
}
return null
}
Surface.prototype.validateEditorType = function(type) {
if (type === undefined) {
type = 'string'
}
if ($.wn.inspector.propertyEditors[type] === undefined) {
throw new Error('The Inspector editor class "' + type +
'" is not defined in the $.wn.inspector.propertyEditors namespace.')
}
}
Surface.prototype.popupDisplayed = function() {
if (this.popupCounter === 0 && this.options.onPopupDisplayed !== null) {
this.options.onPopupDisplayed()
}
this.popupCounter++
}
Surface.prototype.popupHidden = function() {
this.popupCounter--
if (this.popupCounter < 0) {
this.popupCounter = 0
}
if (this.popupCounter === 0 && this.options.onPopupHidden !== null) {
this.options.onPopupHidden()
}
}
Surface.prototype.getInspectableElement = function() {
if (this.options.onGetInspectableElement !== null) {
return this.options.onGetInspectableElement()
}
}
Surface.prototype.getPropertyPath = function(propertyName) {
var result = [],
current = this
result.push(propertyName)
while (current) {
if (current.propertyName) {
result.push(current.propertyName)
}
current = current.parentSurface
}
result.reverse()
return result.join('.')
}
Surface.prototype.findDependentProperties = function(propertyName) {
var dependents = []
for (var i in this.rawProperties) {
var property = this.rawProperties[i]
if (!property.depends) {
continue
}
if (property.depends.indexOf(propertyName) !== -1) {
dependents.push(property.property)
}
}
return dependents
}
//
// Nested surfaces support
//
Surface.prototype.mergeChildSurface = function(surface, mergeAfterRow) {
var rows = surface.tableContainer.querySelectorAll('table.inspector-fields > tbody > tr')
surface.tableContainer = this.getRootSurface().tableContainer
for (var i = rows.length-1; i >= 0; i--) {
var row = rows[i]
mergeAfterRow.parentNode.insertBefore(row, mergeAfterRow.nextSibling)
this.applyGroupLevelToRow(row, surface.group)
}
}
Surface.prototype.getRowHeadElement = function(row) {
for (var i = row.children.length-1; i >= 0; i--) {
var element = row.children[i]
if (element.tagName === 'TH') {
return element
}
}
return null
}
Surface.prototype.getInspectorUniqueId = function() {
return this.inspectorUniqueId
}
Surface.prototype.getRootSurface = function() {
var current = this
while (current) {
if (!current.parentSurface) {
return current
}
current = current.parentSurface
}
}
//
// Disposing
//
Surface.prototype.removeElements = function() {
if (!this.parentSurface) {
this.tableContainer.parentNode.removeChild(this.tableContainer);
}
}
Surface.prototype.disposeEditors = function() {
for (var i = 0, len = this.editors.length; i < len; i++) {
var editor = this.editors[i]
editor.dispose()
}
}
Surface.prototype.disposeExternalParameterEditors = function() {
for (var i = 0, len = this.externalParameterEditors.length; i < len; i++) {
var editor = this.externalParameterEditors[i]
editor.dispose()
}
}
Surface.prototype.disposeControls = function() {
var tooltipControls = this.tableContainer.querySelectorAll('.with-tooltip')
for (var i = 0, len = tooltipControls.length; i < len; i++) {
$(tooltipControls[i]).tooltip('destroy')
}
}
//
// Helpers
//
Surface.prototype.escapeJavascriptString = function(str) {
var div = document.createElement('div')
div.appendChild(document.createTextNode(str))
return div.innerHTML
}
Surface.prototype.comparePropertyValues = function(oldValue, newValue) {
if (oldValue === undefined && newValue !== undefined) {
return false
}
if (oldValue !== undefined && newValue === undefined) {
return false
}
if (typeof oldValue == 'object' && typeof newValue == 'object') {
return JSON.stringify(oldValue) == JSON.stringify(newValue)
}
return oldValue == newValue
}
Surface.prototype.getRootTable = function() {
return this.getRootSurface().container.querySelector('table.inspector-fields')
}
//
// External API
//
Surface.prototype.getValues = function() {
var result = {}
for (var i=0, len = this.parsedProperties.properties.length; i < len; i++) {
var property = this.parsedProperties.properties[i]
if (property.itemType !== 'property') {
continue
}
var value = null,
externalParameterEditor = this.findExternalParameterEditor(property.property)
if (!externalParameterEditor || !externalParameterEditor.isEditorVisible()) {
value = this.getPropertyValue(property.property)
var editor = this.findPropertyEditor(property.property)
if (value === undefined) {
if (editor) {
value = editor.getUndefinedValue()
}
else {
value = property.default
}
}
if (value === $.wn.inspector.removedProperty) {
continue
}
if (property.ignoreIfEmpty !== undefined && (property.ignoreIfEmpty === true || property.ignoreIfEmpty === "true") && editor) {
if (editor.isEmptyValue(value)) {
continue
}
}
if (property.ignoreIfDefault !== undefined && (property.ignoreIfDefault === true || property.ignoreIfDefault === "true") && editor) {
if (property.default === undefined) {
throw new Error('The ignoreIfDefault feature cannot be used without the default property value.')
}
if (this.comparePropertyValues(value, property.default)) {
continue
}
}
}
else {
value = externalParameterEditor.getValue()
value = '{{ ' + value + ' }}'
}
result[property.property] = value
}
return result
}
Surface.prototype.getValidValues = function() {
var allValues = this.getValues(),
result = {}
for (var property in allValues) {
var editor = this.findPropertyEditor(property)
if (!editor) {
throw new Error('Cannot find editor for property ' + property)
}
var externalEditor = this.findExternalParameterEditor(property)
if (externalEditor && externalEditor.isEditorVisible() && !externalEditor.validate(true)) {
result[property] = $.wn.inspector.invalidProperty
continue
}
if (!editor.validate(true)) {
result[property] = $.wn.inspector.invalidProperty
continue
}
result[property] = allValues[property]
}
return result
}
Surface.prototype.validate = function(silentMode) {
this.getGroupManager().unmarkInvalidGroups(this.getRootTable())
for (var i = 0, len = this.editors.length; i < len; i++) {
var editor = this.editors[i],
externalEditor = this.findExternalParameterEditor(editor.propertyDefinition.property)
if (externalEditor && externalEditor.isEditorVisible()) {
if (!externalEditor.validate(silentMode)) {
if (!silentMode) {
editor.markInvalid()
}
return false
}
else {
continue
}
}
if (!editor.validate(silentMode)) {
if (!silentMode) {
editor.markInvalid()
}
return false
}
}
return true
}
Surface.prototype.hasChanges = function(originalValues) {
var values = originalValues !== undefined ? originalValues : this.originalValues
return !this.comparePropertyValues(values, this.getValues())
}
// EVENT HANDLERS
//
Surface.prototype.onGroupClick = function(ev) {
var row = ev.currentTarget
this.toggleGroup(row)
$.wn.foundation.event.stop(ev)
return false
}
// DEFAULT OPTIONS
// ============================
Surface.DEFAULTS = {
enableExternalParameterEditor: false,
onChange: null,
onPopupDisplayed: null,
onPopupHidden: null,
onGetInspectableElement: null
}
// REGISTRATION
// ============================
$.wn.inspector.surface = Surface
$.wn.inspector.removedProperty = {removed: true}
$.wn.inspector.invalidProperty = {invalid: true}
}(window.jQuery);

View File

@@ -0,0 +1,119 @@
/*
* Inspector validation set class.
*/
+function ($) { "use strict";
// NAMESPACES
// ============================
if ($.wn.inspector.validators === undefined)
$.wn.inspector.validators = {}
// CLASS DEFINITION
// ============================
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var ValidationSet = function(options, propertyName) {
this.validators = []
this.options = options
this.propertyName = propertyName
Base.call(this)
this.createValidators()
}
ValidationSet.prototype = Object.create(BaseProto)
ValidationSet.prototype.constructor = Base
ValidationSet.prototype.dispose = function() {
this.disposeValidators()
this.validators = null
BaseProto.dispose.call(this)
}
ValidationSet.prototype.disposeValidators = function() {
for (var i = 0, len = this.validators.length; i < len; i++) {
this.validators[i].dispose()
}
}
ValidationSet.prototype.throwError = function(errorMessage) {
throw new Error(errorMessage + ' Property: ' + this.propertyName)
}
ValidationSet.prototype.createValidators = function() {
// Handle legacy validation syntax properties:
//
// - required
// - validationPattern
// - validationMessage
if ((this.options.required !== undefined ||
this.options.validationPattern !== undefined ||
this.options.validationMessage !== undefined) &&
this.options.validation !== undefined) {
this.throwError('Legacy and new validation syntax should not be mixed.')
}
if (this.options.required !== undefined && this.options.required) {
var validator = new $.wn.inspector.validators.required({
message: this.options.validationMessage
})
this.validators.push(validator)
}
if (this.options.validationPattern !== undefined) {
var validator = new $.wn.inspector.validators.regex({
message: this.options.validationMessage,
pattern: this.options.validationPattern
})
this.validators.push(validator)
}
//
// Handle new validation syntax
//
if (this.options.validation === undefined) {
return
}
for (var validatorName in this.options.validation) {
if ($.wn.inspector.validators[validatorName] == undefined) {
this.throwError('Inspector validator "' + validatorName + '" is not found in the $.wn.inspector.validators namespace.')
}
var validator = new $.wn.inspector.validators[validatorName](
this.options.validation[validatorName]
)
this.validators.push(validator)
}
}
ValidationSet.prototype.validate = function(value) {
try {
for (var i = 0, len = this.validators.length; i < len; i++) {
var validator = this.validators[i],
errorMessage = validator.isValid(value)
if (typeof errorMessage === 'string') {
return errorMessage
}
}
return null
}
catch (err) {
this.throwError(err)
}
}
$.wn.inspector.validationSet = ValidationSet
}(window.jQuery);

View File

@@ -0,0 +1,59 @@
/*
* Inspector validator base class.
*/
+function ($) { "use strict";
// NAMESPACES
// ============================
if ($.wn.inspector.validators === undefined)
$.wn.inspector.validators = {}
// CLASS DEFINITION
// ============================
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var BaseValidator = function(options) {
this.options = options
this.defaultMessage = 'Invalid property value.'
Base.call(this)
}
BaseValidator.prototype = Object.create(BaseProto)
BaseValidator.prototype.constructor = Base
BaseValidator.prototype.dispose = function() {
this.defaultMessage = null
BaseProto.dispose.call(this)
}
BaseValidator.prototype.getMessage = function(defaultMessage) {
if (this.options.message !== undefined) {
return this.options.message
}
if (defaultMessage !== undefined) {
return defaultMessage
}
return this.defaultMessage
}
BaseValidator.prototype.isScalar = function(value) {
if (value === undefined || value === null) {
return true
}
return !!(typeof value === 'string' || typeof value == 'number' || typeof value == 'boolean');
}
BaseValidator.prototype.isValid = function(value) {
return null
}
$.wn.inspector.validators.base = BaseValidator
}(window.jQuery);

View File

@@ -0,0 +1,45 @@
/*
* Base class for Inspector numeric validators.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.validators.base,
BaseProto = Base.prototype
var BaseNumber = function(options) {
Base.call(this, options)
}
BaseNumber.prototype = Object.create(BaseProto)
BaseNumber.prototype.constructor = Base
BaseNumber.prototype.doCommonChecks = function(value) {
if (this.options.min !== undefined || this.options.max !== undefined) {
if (this.options.min !== undefined) {
if (this.options.min.value === undefined) {
throw new Error('The min.value parameter is not defined in the Inspector validator configuration')
}
if (value < this.options.min.value) {
return this.options.min.message !== undefined ?
this.options.min.message :
"The value should not be less than " + this.options.min.value
}
}
if (this.options.max !== undefined) {
if (this.options.max.value === undefined) {
throw new Error('The max.value parameter is not defined in the table Inspector validator configuration')
}
if (value > this.options.max.value) {
return this.options.max.message !== undefined ?
this.options.max.message :
"The value should not be greater than " + this.options.max.value
}
}
}
}
$.wn.inspector.validators.baseNumber = BaseNumber
}(window.jQuery);

View File

@@ -0,0 +1,47 @@
/*
* Inspector float validator.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.validators.baseNumber,
BaseProto = Base.prototype
var FloatValidator = function(options) {
Base.call(this, options)
}
FloatValidator.prototype = Object.create(BaseProto)
FloatValidator.prototype.constructor = Base
FloatValidator.prototype.isValid = function(value) {
if (!this.isScalar(value) || typeof value == 'boolean') {
this.throwError('The Float Inspector validator can only be used with string values.')
}
if (value === undefined || value === null) {
return null
}
var string = $.trim(String(value))
if (string.length === 0) {
return null
}
var testResult = this.options.allowNegative ?
/^[-]?([0-9]+\.[0-9]+|[0-9]+)$/.test(string) :
/^([0-9]+\.[0-9]+|[0-9]+)$/.test(string)
if (!testResult) {
var defaultMessage = this.options.allowNegative ?
'The value should be a floating point number.' :
'The value should be a positive floating point number.';
return this.getMessage(defaultMessage)
}
return this.doCommonChecks(parseFloat(string))
}
$.wn.inspector.validators.float = FloatValidator
}(window.jQuery);

View File

@@ -0,0 +1,47 @@
/*
* Inspector integer validator.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.validators.baseNumber,
BaseProto = Base.prototype
var IntegerValidator = function(options) {
Base.call(this, options)
}
IntegerValidator.prototype = Object.create(BaseProto)
IntegerValidator.prototype.constructor = Base
IntegerValidator.prototype.isValid = function(value) {
if (!this.isScalar(value) || typeof value == 'boolean') {
this.throwError('The Integer Inspector validator can only be used with string values.')
}
if (value === undefined || value === null) {
return null
}
var string = $.trim(String(value))
if (string.length === 0) {
return null
}
var testResult = this.options.allowNegative ?
/^\-?[0-9]*$/.test(string) :
/^[0-9]*$/.test(string)
if (!testResult) {
var defaultMessage = this.options.allowNegative ?
'The value should be an integer.' :
'The value should be a positive integer.';
return this.getMessage(defaultMessage)
}
return this.doCommonChecks(parseInt(string))
}
$.wn.inspector.validators.integer = IntegerValidator
}(window.jQuery);

View File

@@ -0,0 +1,72 @@
/*
* Inspector length validator.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.validators.base,
BaseProto = Base.prototype
var LengthValidator = function(options) {
Base.call(this, options)
}
LengthValidator.prototype = Object.create(BaseProto)
LengthValidator.prototype.constructor = Base
LengthValidator.prototype.isValid = function(value) {
if (value === undefined || value === null) {
return null
}
if (typeof value == 'boolean') {
this.throwError('The Length Inspector validator cannot work with Boolean values.')
}
var length = null
if(Object.prototype.toString.call(value) === '[object Array]' || typeof value === 'string') {
length = value.length
}
else if (typeof value === 'object') {
length = this.getObjectLength(value)
}
if (this.options.min !== undefined || this.options.max !== undefined) {
if (this.options.min !== undefined) {
if (this.options.min.value === undefined) {
throw new Error('The min.value parameter is not defined in the Length Inspector validator configuration.')
}
if (length < this.options.min.value) {
return this.options.min.message !== undefined ?
this.options.min.message :
"The value should not be shorter than " + this.options.min.value
}
}
if (this.options.max !== undefined) {
if (this.options.max.value === undefined)
throw new Error('The max.value parameter is not defined in the Length Inspector validator configuration.')
if (length > this.options.max.value) {
return this.options.max.message !== undefined ?
this.options.max.message :
"The value should not be longer than " + this.options.max.value
}
}
}
}
LengthValidator.prototype.getObjectLength = function(value) {
var result = 0
for (var key in value) {
result++
}
return result
}
$.wn.inspector.validators.length = LengthValidator
}(window.jQuery);

View File

@@ -0,0 +1,41 @@
/*
* Inspector regex validator.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.validators.base,
BaseProto = Base.prototype
var RegexValidator = function(options) {
Base.call(this, options)
}
RegexValidator.prototype = Object.create(BaseProto)
RegexValidator.prototype.constructor = Base
RegexValidator.prototype.isValid = function(value) {
if (this.options.pattern === undefined) {
this.throwError('The pattern parameter is not defined in the Regex Inspector validator configuration.')
}
if (!this.isScalar(value)) {
this.throwError('The Regex Inspector validator can only be used with string values.')
}
if (value === undefined || value === null) {
return null
}
var string = $.trim(String(value))
if (string.length === 0) {
return null
}
var regexObj = new RegExp(this.options.pattern, this.options.modifiers)
return regexObj.test(string) ? null : this.getMessage()
}
$.wn.inspector.validators.regex = RegexValidator
}(window.jQuery);

View File

@@ -0,0 +1,35 @@
/*
* Inspector required validator.
*/
+function ($) { "use strict";
var Base = $.wn.inspector.validators.base,
BaseProto = Base.prototype
var RequiredValidator = function(options) {
Base.call(this, options)
this.defaultMessage = 'The property is required.'
}
RequiredValidator.prototype = Object.create(BaseProto)
RequiredValidator.prototype.constructor = Base
RequiredValidator.prototype.isValid = function(value) {
if (value === undefined || value === null) {
return this.getMessage()
}
if (typeof value === 'boolean') {
return value ? null : this.getMessage()
}
if (typeof value === 'object') {
return !$.isEmptyObject(value) ? null : this.getMessage()
}
return $.trim(String(value)).length > 0 ? null : this.getMessage()
}
$.wn.inspector.validators.required = RequiredValidator
}(window.jQuery);

View File

@@ -0,0 +1,412 @@
/*
* Inspector wrapper base class.
*/
+function ($) { "use strict";
// NAMESPACES
// ============================
if ($.wn.inspector === undefined)
$.wn.inspector = {}
if ($.wn.inspector.wrappers === undefined)
$.wn.inspector.wrappers = {}
// CLASS DEFINITION
// ============================
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var BaseWrapper = function($element, sourceWrapper, options) {
this.$element = $element
this.options = $.extend({}, BaseWrapper.DEFAULTS, typeof options == 'object' && options)
this.switched = false
this.configuration = null
Base.call(this)
if (!sourceWrapper) {
if (!this.triggerShowingAndInit()) {
// this.init() is called inside triggerShowing()
return
}
this.surface = null
this.title = null
this.description = null
}
else {
this.surface = sourceWrapper.surface
this.title = sourceWrapper.title
this.description = sourceWrapper.description
sourceWrapper = null
this.init()
}
}
BaseWrapper.prototype = Object.create(BaseProto)
BaseWrapper.prototype.constructor = Base
BaseWrapper.prototype.dispose = function() {
if (!this.switched) {
this.$element.removeClass('inspector-open')
this.setInspectorVisibleFlag(false)
this.$element.trigger('hidden.oc.inspector')
}
if (this.surface !== null && this.surface.options.onGetInspectableElement === this.proxy(this.onGetInspectableElement)) {
this.surface.options.onGetInspectableElement = null
}
this.surface = null
this.$element = null
this.title = null
this.description = null
this.configuration = null
BaseProto.dispose.call(this)
}
BaseWrapper.prototype.init = function() {
// Wrappers can create a new surface or inject an existing
// surface to the UI they manage.
//
// If there is no surface provided in the wrapper constructor,
// the wrapper first loads the Inspector configuration and values
// and then calls the createSurfaceAndUi() method with all information
// required for creating a new Inspector surface and UI.
if (!this.surface) {
this.loadConfiguration()
}
else {
this.adoptSurface()
}
this.$element.addClass('inspector-open')
}
//
// Helper methods
//
BaseWrapper.prototype.getElementValuesInput = function() {
return this.$element.find('> input[data-inspector-values]')
}
BaseWrapper.prototype.normalizePropertyCode = function(code, configuration) {
var lowerCaseCode = code.toLowerCase()
for (var index in configuration) {
var propertyInfo = configuration[index]
if (propertyInfo.property.toLowerCase() == lowerCaseCode) {
return propertyInfo.property
}
}
return code
}
BaseWrapper.prototype.isExternalParametersEditorEnabled = function() {
return this.$element.closest('[data-inspector-external-parameters]').length > 0
}
BaseWrapper.prototype.initSurface = function(containerElement, properties, values) {
var options = this.$element.data() || {}
options.enableExternalParameterEditor = this.isExternalParametersEditorEnabled()
options.onGetInspectableElement = this.proxy(this.onGetInspectableElement)
this.surface = new $.wn.inspector.surface(
containerElement,
properties,
values,
$.wn.inspector.helpers.generateElementUniqueId(this.$element.get(0)),
options)
}
BaseWrapper.prototype.isLiveUpdateEnabled = function() {
return false
}
//
// Wrapper API
//
BaseWrapper.prototype.createSurfaceAndUi = function(properties, values) {
}
BaseWrapper.prototype.setInspectorVisibleFlag = function(value) {
this.$element.data('oc.inspectorVisible', value)
}
BaseWrapper.prototype.adoptSurface = function() {
this.surface.options.onGetInspectableElement = this.proxy(this.onGetInspectableElement)
}
BaseWrapper.prototype.cleanupAfterSwitch = function() {
this.switched = true
this.dispose()
}
//
// Values
//
BaseWrapper.prototype.loadValues = function(configuration) {
var $valuesField = this.getElementValuesInput()
if ($valuesField.length > 0) {
var valuesStr = $.trim($valuesField.val())
try {
return valuesStr.length === 0 ? {} : JSON.parse(valuesStr)
}
catch (err) {
throw new Error('Error parsing Inspector field values. ' + err)
}
}
var values = {},
attributes = this.$element.get(0).attributes
for (var i=0, len = attributes.length; i < len; i++) {
var attribute = attributes[i],
matches = []
if (matches = attribute.name.match(/^data-property-(.*)$/)) {
// Important - values contained in data-property-xxx attributes are
// considered strings and never parsed with JSON. The use of the
// data-property-xxx attributes is very limited - they're only
// used in Pages for creating snippets from partials, where properties
// are created with a table UI widget, which doesn't allow creating
// properties of any complex types.
//
// There is no a technically reliable way to determine when a string
// is a JSON data or a regular string. Users can enter a value
// like [10], which is a proper JSON value, but meant to be a string.
//
// One possible way to resolve it, if to check the property type loaded
// from the configuration and see if the corresponding editor expects
// complex data.
var normalizedPropertyName = this.normalizePropertyCode(matches[1], configuration)
values[normalizedPropertyName] = attribute.value
}
}
return values
}
BaseWrapper.prototype.applyValues = function(liveUpdateMode) {
var $valuesField = this.getElementValuesInput(),
values = liveUpdateMode ?
this.surface.getValidValues() :
this.surface.getValues()
if (liveUpdateMode) {
// In the live update mode, when only valid values are applied,
// we don't want to change all other values (invalid properties).
var existingValues = this.loadValues(this.configuration)
for (var property in values) {
if (values[property] !== $.wn.inspector.invalidProperty) {
existingValues[property] = values[property]
}
}
// Properties that use settings like ignoreIfPropertyEmpty could
// be removed from the list returned by getValidValues(). Removed
// properties should be removed from the result list.
var filteredValues = {}
for (var property in existingValues) {
if (values.hasOwnProperty(property)) {
filteredValues[property] = existingValues[property]
}
}
values = filteredValues
}
if ($valuesField.length > 0) {
$valuesField.val(JSON.stringify(values))
}
else {
for (var property in values) {
var value = values[property]
if ($.isArray(value) ||$.isPlainObject(value)) {
throw new Error('Inspector data-property-xxx attributes do not support complex values. Property: ' + property)
}
this.$element.attr('data-property-' + property, value)
}
}
// In the live update mode the livechange event is triggered
// regardless of whether Surface properties match or don't match
// the original properties of the inspectable element. Without it
// there could be undesirable side effects.
if (liveUpdateMode) {
this.$element.trigger('livechange')
}
else {
var hasChanges = false
if (this.isLiveUpdateEnabled()) {
var currentValues = this.loadValues(this.configuration)
// If the Inspector setup supports the live update mode,
// evaluate changes as a difference between the current element
// properties and internal properties stored in the Surface.
// If there is no differences, the properties have already
// been applied with a preceding live update.
hasChanges = this.surface.hasChanges(currentValues)
}
else {
hasChanges = this.surface.hasChanges()
}
if (hasChanges) {
this.$element.trigger('change')
}
}
}
//
// Configuration
//
BaseWrapper.prototype.loadConfiguration = function() {
var configString = this.$element.data('inspector-config'),
result = {
properties: {},
title: null,
description: null
}
result.title = this.$element.data('inspector-title')
result.description = this.$element.data('inspector-description')
if (configString !== undefined) {
result.properties = this.parseConfiguration(configString)
this.configurationLoaded(result)
return
}
var $configurationField = this.$element.find('> input[data-inspector-config]')
if ($configurationField.length > 0) {
result.properties = this.parseConfiguration($configurationField.val())
this.configurationLoaded(result)
return
}
var $form = this.$element.closest('form'),
data = this.$element.data(),
self = this
$.wn.stripeLoadIndicator.show()
$form.request('onGetInspectorConfiguration', {
data: data
}).done(function inspectorConfigurationRequestDoneClosure(data) {
self.onConfigurartionRequestDone(data, result)
}).always(function() {
$.wn.stripeLoadIndicator.hide()
})
}
BaseWrapper.prototype.parseConfiguration = function(configuration) {
if (!$.isArray(configuration) && !$.isPlainObject(configuration)) {
if ($.trim(configuration) === 0) {
return {}
}
try {
return JSON.parse(configuration)
}
catch(err) {
throw new Error('Error parsing Inspector configuration. ' + err)
}
}
else {
return configuration
}
}
BaseWrapper.prototype.configurationLoaded = function(configuration) {
var values = this.loadValues(configuration.properties)
this.title = configuration.title
this.description = configuration.description
this.configuration = configuration
this.createSurfaceAndUi(configuration.properties, values)
}
BaseWrapper.prototype.onConfigurartionRequestDone = function(data, result) {
result.properties = this.parseConfiguration(data.configuration.properties)
if (data.configuration.title !== undefined) {
result.title = data.configuration.title
}
if (data.configuration.description !== undefined) {
result.description = data.configuration.description
}
this.configurationLoaded(result)
}
//
// Events
//
BaseWrapper.prototype.triggerShowingAndInit = function() {
var e = $.Event('showing.oc.inspector')
this.$element.trigger(e, [{callback: this.proxy(this.init)}])
if (e.isDefaultPrevented()) {
this.$element = null
return false
}
if (!e.isPropagationStopped()) {
this.init()
}
}
BaseWrapper.prototype.triggerHiding = function() {
var hidingEvent = $.Event('hiding.oc.inspector'),
values = this.surface.getValues()
this.$element.trigger(hidingEvent, [{values: values}])
return !hidingEvent.isDefaultPrevented();
}
BaseWrapper.prototype.onGetInspectableElement = function() {
return this.$element
}
BaseWrapper.DEFAULTS = {
containerSupported: false
}
$.wn.inspector.wrappers.base = BaseWrapper
}(window.jQuery);

View File

@@ -0,0 +1,245 @@
/*
* Inspector container wrapper.
*/
+function ($) { "use strict";
// CLASS DEFINITION
// ============================
var Base = $.wn.inspector.wrappers.base,
BaseProto = Base.prototype
var InspectorContainer = function($element, surface, options) {
if (!options.container) {
throw new Error('Cannot create Inspector container wrapper without a container element.')
}
this.surfaceContainer = null
Base.call(this, $element, surface, options)
}
InspectorContainer.prototype = Object.create(BaseProto)
InspectorContainer.prototype.constructor = Base
InspectorContainer.prototype.init = function() {
this.registerHandlers()
BaseProto.init.call(this)
}
InspectorContainer.prototype.dispose = function() {
this.unregisterHandlers()
this.removeControls()
this.surfaceContainer = null
BaseProto.dispose.call(this)
}
InspectorContainer.prototype.createSurfaceAndUi = function(properties, values) {
this.buildUi()
this.initSurface(this.surfaceContainer, properties, values)
if (this.isLiveUpdateEnabled()) {
this.surface.options.onChange = this.proxy(this.onLiveUpdate)
}
}
InspectorContainer.prototype.adoptSurface = function() {
this.buildUi()
this.surface.moveToContainer(this.surfaceContainer)
if (this.isLiveUpdateEnabled()) {
this.surface.options.onChange = this.proxy(this.onLiveUpdate)
}
BaseProto.adoptSurface.call(this)
}
InspectorContainer.prototype.buildUi = function() {
var scrollable = this.isScrollable(),
head = this.buildHead(),
layoutElements = this.buildLayout()
layoutElements.headContainer.appendChild(head)
if (scrollable) {
var scrollpad = this.buildScrollpad()
this.surfaceContainer = scrollpad.container
layoutElements.bodyContainer.appendChild(scrollpad.scrollpad)
$(scrollpad.scrollpad).scrollpad()
}
else {
this.surfaceContainer = layoutElements.bodyContainer
}
this.setInspectorVisibleFlag(true)
}
InspectorContainer.prototype.buildHead = function() {
var container = document.createElement('div'),
header = document.createElement('h3'),
paragraph = document.createElement('p'),
detachButton = document.createElement('span'),
closeButton = document.createElement('span')
container.setAttribute('class', 'inspector-header')
detachButton.setAttribute('class', 'wn-icon-external-link-square detach')
closeButton.setAttribute('class', 'close')
header.textContent = this.title
paragraph.textContent = this.description
closeButton.innerHTML = '&times;';
container.appendChild(header)
container.appendChild(paragraph)
container.appendChild(detachButton)
container.appendChild(closeButton)
return container
}
InspectorContainer.prototype.buildScrollpad = function() {
var scrollpad = document.createElement('div'),
scrollWrapper = document.createElement('div'),
scrollableContainer = document.createElement('div')
scrollpad.setAttribute('class', 'control-scrollpad')
scrollpad.setAttribute('data-control', 'scrollpad')
scrollWrapper.setAttribute('class', 'scroll-wrapper inspector-wrapper')
scrollpad.appendChild(scrollWrapper)
scrollWrapper.appendChild(scrollableContainer)
return {
scrollpad: scrollpad,
container: scrollableContainer
}
}
InspectorContainer.prototype.buildLayout = function() {
var layout = document.createElement('div'),
headRow = document.createElement('div'),
bodyRow = document.createElement('div')
layout.setAttribute('class', 'flex-layout-column fill-container')
headRow.setAttribute('class', 'flex-layout-item fix')
bodyRow.setAttribute('class', 'flex-layout-item stretch relative')
layout.appendChild(headRow)
layout.appendChild(bodyRow)
this.options.container.get(0).appendChild(layout)
$.wn.foundation.controlUtils.markDisposable(layout)
this.registerLayoutHandlers(layout)
return {
headContainer: headRow,
bodyContainer: bodyRow
}
}
InspectorContainer.prototype.validateAndApply = function() {
if (!this.surface.validate()) {
return false
}
this.applyValues()
return true
}
InspectorContainer.prototype.isScrollable = function() {
return this.options.container.data('inspector-scrollable') !== undefined
}
InspectorContainer.prototype.isLiveUpdateEnabled = function() {
return this.options.container.data('inspector-live-update') !== undefined
}
InspectorContainer.prototype.getLayout = function() {
return this.options.container.get(0).querySelector('div.flex-layout-column')
}
InspectorContainer.prototype.registerLayoutHandlers = function(layout) {
var $layout = $(layout)
$layout.one('dispose-control', this.proxy(this.dispose))
$layout.on('click', 'span.close', this.proxy(this.onClose))
$layout.on('click', 'span.detach', this.proxy(this.onDetach))
}
InspectorContainer.prototype.registerHandlers = function() {
this.options.container.on('apply.oc.inspector', this.proxy(this.onApplyValues))
this.options.container.on('beforeContainerHide.oc.inspector', this.proxy(this.onBeforeHide))
}
InspectorContainer.prototype.unregisterHandlers = function() {
var $layout = $(this.getLayout())
this.options.container.off('apply.oc.inspector', this.proxy(this.onApplyValues))
this.options.container.off('beforeContainerHide.oc.inspector', this.proxy(this.onBeforeHide))
$layout.off('dispose-control', this.proxy(this.dispose))
$layout.off('click', 'span.close', this.proxy(this.onClose))
$layout.off('click', 'span.detach', this.proxy(this.onDetach))
if (this.surface !== null && this.surface.options.onChange === this.proxy(this.onLiveUpdate)) {
this.surface.options.onChange = null
}
}
InspectorContainer.prototype.removeControls = function() {
if (this.isScrollable()) {
this.options.container.find('.control-scrollpad').scrollpad('dispose')
}
var layout = this.getLayout()
layout.parentNode.removeChild(layout)
}
InspectorContainer.prototype.onApplyValues = function(ev) {
if (!this.validateAndApply()) {
ev.preventDefault()
return false
}
}
InspectorContainer.prototype.onBeforeHide = function(ev) {
if (!this.triggerHiding()) {
ev.preventDefault()
return false
}
}
InspectorContainer.prototype.onClose = function(ev) {
if (!this.validateAndApply()) {
ev.preventDefault()
return false
}
if (!this.triggerHiding()) {
ev.preventDefault()
return false
}
this.surface.dispose()
this.dispose()
}
InspectorContainer.prototype.onLiveUpdate = function() {
this.applyValues(true)
}
InspectorContainer.prototype.onDetach = function() {
$.wn.inspector.manager.switchToPopup(this)
}
$.wn.inspector.wrappers.container = InspectorContainer
}(window.jQuery);

View File

@@ -0,0 +1,217 @@
/*
* Inspector popup wrapper.
*/
+function ($) { "use strict";
// CLASS DEFINITION
// ============================
var Base = $.wn.inspector.wrappers.base,
BaseProto = Base.prototype
var InspectorPopup = function($element, surface, options) {
this.$popoverContainer = null
this.popoverObj = null
this.cleaningUp = false
Base.call(this, $element, surface, options)
}
InspectorPopup.prototype = Object.create(BaseProto)
InspectorPopup.prototype.constructor = Base
InspectorPopup.prototype.dispose = function() {
this.unregisterHandlers()
this.$popoverContainer = null
this.popoverObj = null
BaseProto.dispose.call(this)
}
InspectorPopup.prototype.createSurfaceAndUi = function(properties, values, title, description) {
this.showPopover()
this.initSurface(this.$popoverContainer.find('[data-surface-container]').get(0), properties, values)
this.repositionPopover()
this.registerPopupHandlers()
}
InspectorPopup.prototype.adoptSurface = function() {
this.showPopover()
this.surface.moveToContainer(this.$popoverContainer.find('[data-surface-container]').get(0))
this.repositionPopover()
this.registerPopupHandlers()
BaseProto.adoptSurface.call(this)
}
InspectorPopup.prototype.cleanupAfterSwitch = function() {
this.cleaningUp = true
this.switched = true
this.forceClose()
// The parent cleanupAfterSwitch() is not called because
// disposing happens in onHide() triggered by forceClose()
}
InspectorPopup.prototype.getPopoverContents = function() {
return '<div class="popover-head"> \
<h3 data-inspector-title></h3> \
<p data-inspector-description></p> \
<button type="button" class="close" \
data-dismiss="popover" \
aria-hidden="true">&times;</button> \
</div> \
<form autocomplete="off" onsubmit="return false"> \
<div data-surface-container></div> \
<form>'
}
InspectorPopup.prototype.showPopover = function() {
var offset = this.$element.data('inspector-offset'),
offsetX = this.$element.data('inspector-offset-x'),
offsetY = this.$element.data('inspector-offset-y'),
placement = this.$element.data('inspector-placement'),
fallbackPlacement = this.$element.data('inspector-fallback-placement')
if (offset === undefined) {
offset = 15
}
if (placement === undefined) {
placement = 'bottom'
}
if (fallbackPlacement === undefined) {
fallbackPlacement = 'bottom'
}
this.$element.ocPopover({
content: this.getPopoverContents(),
highlightModalTarget: true,
modal: true,
placement: placement,
fallbackPlacement: fallbackPlacement,
containerClass: 'control-inspector',
container: this.$element.data('inspector-container'),
offset: offset,
offsetX: offsetX,
offsetY: offsetY,
width: 400
})
this.setInspectorVisibleFlag(true)
this.popoverObj = this.$element.data('oc.popover')
this.$popoverContainer = this.popoverObj.$container
this.$popoverContainer.addClass('inspector-temporary-placement')
if (this.options.inspectorCssClass !== undefined) {
this.$popoverContainer.addClass(this.options.inspectorCssClass)
}
if (this.options.containerSupported) {
var moveToContainerButton = $('<span class="inspector-move-to-container wn-icon-download">')
this.$popoverContainer.find('.popover-head').append(moveToContainerButton)
}
this.$popoverContainer.find('[data-inspector-title]').text(this.title)
this.$popoverContainer.find('[data-inspector-description]').text(this.description)
}
InspectorPopup.prototype.repositionPopover = function() {
this.popoverObj.reposition()
this.$popoverContainer.removeClass('inspector-temporary-placement')
this.$popoverContainer.find('div[data-surface-container] > div').trigger('focus-control')
}
InspectorPopup.prototype.forceClose = function() {
this.$popoverContainer.trigger('close.oc.popover')
}
InspectorPopup.prototype.registerPopupHandlers = function() {
this.surface.options.onPopupDisplayed = this.proxy(this.onPopupEditorDisplayed)
this.surface.options.onPopupHidden = this.proxy(this.onPopupEditorHidden)
this.popoverObj.options.onCheckDocumentClickTarget = this.proxy(this.onCheckDocumentClickTarget)
this.$element.on('hiding.oc.popover', this.proxy(this.onBeforeHide))
this.$element.on('hide.oc.popover', this.proxy(this.onHide))
this.$popoverContainer.on('keydown', this.proxy(this.onPopoverKeyDown))
if (this.options.containerSupported) {
this.$popoverContainer.on('click', 'span.inspector-move-to-container', this.proxy(this.onMoveToContainer))
}
}
InspectorPopup.prototype.unregisterHandlers = function() {
this.popoverObj.options.onCheckDocumentClickTarget = null
this.$element.off('hiding.oc.popover', this.proxy(this.onBeforeHide))
this.$element.off('hide.oc.popover', this.proxy(this.onHide))
this.$popoverContainer.off('keydown', this.proxy(this.onPopoverKeyDown))
if (this.options.containerSupported) {
this.$popoverContainer.off('click', 'span.inspector-move-to-container', this.proxy(this.onMoveToContainer))
}
this.surface.options.onPopupDisplayed = null
this.surface.options.onPopupHidden = null
}
InspectorPopup.prototype.onBeforeHide = function(ev) {
if (this.cleaningUp) {
return
}
if (!this.surface.validate()) {
ev.preventDefault()
return false
}
if (!this.triggerHiding()) {
ev.preventDefault()
return false
}
this.applyValues()
}
InspectorPopup.prototype.onHide = function(ev) {
this.dispose()
}
InspectorPopup.prototype.onPopoverKeyDown = function(ev) {
if(ev.key === 'Enter') {
$(ev.currentTarget).trigger('close.oc.popover')
}
}
InspectorPopup.prototype.onPopupEditorDisplayed = function() {
this.popoverObj.options.closeOnPageClick = false
this.popoverObj.options.closeOnEsc = false
}
InspectorPopup.prototype.onPopupEditorHidden = function() {
this.popoverObj.options.closeOnPageClick = true
this.popoverObj.options.closeOnEsc = true
}
InspectorPopup.prototype.onCheckDocumentClickTarget = function(element) {
if ($.contains(this.$element, element) || this.$element.get(0) === element) {
return true
}
}
InspectorPopup.prototype.onMoveToContainer = function() {
$.wn.inspector.manager.switchToContainer(this)
}
$.wn.inspector.wrappers.popup = InspectorPopup
}(window.jQuery);

View File

@@ -0,0 +1,127 @@
/*
* Table row linking plugin
*
* Data attributes:
* - data-control="rowlink" - enables the plugin on an element
* - data-exclude-class="nolink" - disables the link for elements with this class
* - data-linked-class="rowlink" - this class is added to affected table rows
*
* JavaScript API:
* $('a#someElement').rowLink()
*
* Dependences:
* - Null
*/
+function ($) { "use strict";
// ROWLINK CLASS DEFINITION
// ============================
var RowLink = function(element, options) {
var self = this
this.options = options
this.$el = $(element)
var tr = this.$el.prop('tagName') == 'TR'
? this.$el
: this.$el.find('tr:has(td)')
tr.each(function(){
var link = $(this).find(options.target).filter(function(){
return !$(this).closest('td').hasClass(options.excludeClass) && !$(this).hasClass(options.excludeClass)
}).first()
if (!link.length) return
var href = link.attr('href'),
onclick = (typeof link.get(0).onclick == "function") ? link.get(0).onclick : null,
popup = link.is('[data-control=popup]'),
request = link.is('[data-request]')
function handleClick(e) {
if ($(document.body).hasClass('drag')) {
return
}
if (onclick) {
onclick.apply(link.get(0))
}
else if (request) {
link.request()
}
else if (popup) {
link.popup()
}
else if (e.ctrlKey || e.metaKey) {
window.open(href)
}
else {
window.location = href
}
}
$(this).not('.' + options.excludeClass).find('td').not('.' + options.excludeClass).click(function (e) {
handleClick(e)
}).mousedown(function (e) {
if (e.which == 2) {
window.open(href)
}
})
$(this).not('.' + options.excludeClass).on('keypress', function(e) {
if (e.key === '(Space character)' || e.key === 'Spacebar' || e.key === ' ') {
handleClick(e)
return false
}
})
$(this).addClass(options.linkedClass)
link.after(link.contents()).hide()
})
// Add Keyboard Navigation to list rows
$('tr.rowlink').attr('tabindex', 0)
}
RowLink.DEFAULTS = {
target: 'a',
excludeClass: 'nolink',
linkedClass: 'rowlink'
}
// ROWLINK PLUGIN DEFINITION
// ============================
var old = $.fn.rowLink
$.fn.rowLink = function (option) {
var args = Array.prototype.slice.call(arguments, 1)
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.rowlink')
var options = $.extend({}, RowLink.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.rowlink', (data = new RowLink(this, options)))
else if (typeof option == 'string') data[option].apply(data, args)
})
}
$.fn.rowLink.Constructor = RowLink
// ROWLINK NO CONFLICT
// =================
$.fn.rowLink.noConflict = function () {
$.fn.rowLink = old
return this
}
// ROWLINK DATA-API
// ===============
$(document).render(function() {
$('[data-control="rowlink"]').rowLink()
})
}(window.jQuery);

View File

@@ -0,0 +1,464 @@
/*
* Sortable plugin.
*
* Status: experimental. The behavior is not perfect, but it's OK in terms of memory
* usage and disposing.
*
* This is a lightweight, Winter-style implementation of the drag & drop sorting
* functionality. The plugin uses only HTML5 Drag&Drop feature and completely
* disposable.
*
* During the dragging the plugin creates a placeholder element, which should be
* styled separately.
*
* Draggable elements should be marked with "draggable" HTML5 attribute.
*
* Current / planned features:
*
* [x] Sorting a single list.
* [ ] Dragging items between multiple lists.
* [ ] Sorting nested lists.
* JAVASCRIPT API
*
* $('#list').listSortable({})
*
* DATA ATTRIBUTES API
*
* In the simplest case the plugin can be initialized like this:
* <ul data-control="list-sortable">
* <li draggable="true">...</li>
*
* Multiple lists will not support this option and the plugin should be created
* and updated by a caller code.
*
* Options:
* - handle: optional selector for a drag handle element. Also available as data-handle attribute.
* - direction: direction of the list - horizontal or vertical. Also available as data-direction attribute. Default is vertical.
*
* Events:
* - dragged.list.sortable - triggered on a list element after it was moved
*/
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype,
listSortableIdCounter = 0,
elementsIdCounter = 0
var ListSortable = function (element, options) {
this.lists = []
this.options = options
this.listSortableId = null
this.lastMousePosition = null
Base.call(this)
$.wn.foundation.controlUtils.markDisposable(element)
this.init()
this.addList(element)
}
ListSortable.prototype = Object.create(BaseProto)
ListSortable.prototype.constructor = ListSortable
ListSortable.prototype.init = function () {
listSortableIdCounter++
this.listSortableId = 'listsortable/id/' + listSortableIdCounter
}
ListSortable.prototype.addList = function(list) {
this.lists.push(list)
this.registerListHandlers(list)
if (this.lists.length == 1) {
$(list).one('dispose-control', this.proxy(this.dispose))
}
}
//
// Event management
//
ListSortable.prototype.registerListHandlers = function(list) {
var $list = $(list)
$list.on('dragstart', '> li', this.proxy(this.onDragStart))
$list.on('dragover', '> li', this.proxy(this.onDragOver))
$list.on('dragenter', '> li', this.proxy(this.onDragEnter))
$list.on('dragleave', '> li', this.proxy(this.onDragLeave))
$list.on('drop', '> li', this.proxy(this.onDragDrop))
$list.on('dragend', '> li', this.proxy(this.onDragEnd))
}
ListSortable.prototype.unregisterListHandlers = function(list) {
var $list = $(list)
$list.off('dragstart', '> li', this.proxy(this.onDragStart))
$list.off('dragover', '> li', this.proxy(this.onDragOver))
$list.off('dragenter', '> li', this.proxy(this.onDragEnter))
$list.off('dragleave', '> li', this.proxy(this.onDragLeave))
$list.off('drop', '> li', this.proxy(this.onDragDrop))
$list.off('dragend', '> li', this.proxy(this.onDragEnd))
}
ListSortable.prototype.unregisterHandlers = function() {
$(document).off('dragover', this.proxy(this.onDocumentDragOver))
$(document).off('mousemove', this.proxy(this.onDocumentMouseMove))
$(this.lists[0]).off('dispose-control', this.proxy(this.dispose))
}
//
// Disposing
//
ListSortable.prototype.unbindLists = function() {
for (var i=this.lists.length-1; i>0; i--) {
var list = this.lists[i]
this.unregisterListHandlers(this.lists[i])
$(list).removeData('oc.listSortable')
}
}
ListSortable.prototype.dispose = function() {
this.unbindLists()
this.unregisterHandlers()
this.options = null
this.lists = []
BaseProto.dispose.call(this)
}
//
// Internal helpers
//
ListSortable.prototype.elementBelongsToManagedList = function(element) {
for (var i=this.lists.length-1; i >= 0; i--) {
var list = this.lists[i],
children = [].slice.call(list.children); // Converts HTMLCollection to array
if (children.indexOf(element) !== -1) {
return true
}
}
return false
}
ListSortable.prototype.isDragStartAllowed = function(element) {
// TODO: if handle selector is specified - test if
// the element is a handle.
return true
}
ListSortable.prototype.elementIsPlaceholder = function(element) {
return element.getAttribute('class') === 'list-sortable-placeholder'
}
ListSortable.prototype.getElementSortableId = function(element) {
if (element.hasAttribute('data-list-sortable-element-id')) {
return element.getAttribute('data-list-sortable-element-id')
}
elementsIdCounter++
var elementId = elementsIdCounter
element.setAttribute('data-list-sortable-element-id', elementsIdCounter)
return elementsIdCounter
}
ListSortable.prototype.dataTransferContains = function(ev, element) {
if (ev.dataTransfer.types.indexOf !== undefined){
return ev.dataTransfer.types.indexOf(element) >= 0
}
return ev.dataTransfer.types.contains(element)
}
ListSortable.prototype.isSourceManagedList = function(ev) {
return this.dataTransferContains(ev, this.listSortableId)
}
ListSortable.prototype.removePlaceholders = function() {
for (var i=this.lists.length-1; i >= 0; i--) {
var list = this.lists[i],
placeholders = list.querySelectorAll('.list-sortable-placeholder')
for (var j=placeholders.length-1; j >= 0; j--) {
list.removeChild(placeholders[j])
}
}
}
ListSortable.prototype.createPlaceholder = function(element, ev) {
var placeholder = document.createElement('li'),
placement = this.getPlaceholderPlacement(element, ev)
this.removePlaceholders()
placeholder.setAttribute('class', 'list-sortable-placeholder')
placeholder.setAttribute('draggable', true)
if (placement == 'before') {
element.parentNode.insertBefore(placeholder, element)
}
else {
element.parentNode.insertBefore(placeholder, element.nextSibling)
}
}
ListSortable.prototype.moveElement = function(target, ev) {
var list = target.parentNode,
placeholder = list.querySelector('.list-sortable-placeholder')
if (!placeholder) {
return
}
var elementId = ev.dataTransfer.getData('listsortable/elementid')
if (!elementId) {
return
}
var item = this.findDraggedItem(elementId)
if (!item) {
return
}
placeholder.parentNode.insertBefore(item, placeholder)
$(item).trigger('dragged.list.sortable')
}
ListSortable.prototype.findDraggedItem = function(elementId) {
for (var i=this.lists.length-1; i >= 0; i--) {
var list = this.lists[i],
item = list.querySelector('[data-list-sortable-element-id="'+elementId+'"]')
if (item) {
return item
}
}
return null
}
ListSortable.prototype.getPlaceholderPlacement = function(hoverElement, ev) {
var mousePosition = $.wn.foundation.event.pageCoordinates(ev),
elementPosition = $.wn.foundation.element.absolutePosition(hoverElement)
if (this.options.direction == 'vertical') {
var elementCenter = elementPosition.top + hoverElement.offsetHeight/2
return mousePosition.y <= elementCenter ? 'before' : 'after'
}
else {
var elementCenter = elementPosition.left + hoverElement.offsetWidth/2
return mousePosition.x <= elementCenter ? 'before' : 'after'
}
}
ListSortable.prototype.lastMousePositionChanged = function(ev) {
var mousePosition = $.wn.foundation.event.pageCoordinates(ev.originalEvent)
if (this.lastMousePosition === null || this.lastMousePosition.x != mousePosition.x || this.lastMousePosition.y != mousePosition.y) {
this.lastMousePosition = mousePosition
return true
}
return false
}
ListSortable.prototype.mouseOutsideLists = function(ev) {
var mousePosition = $.wn.foundation.event.pageCoordinates(ev)
for (var i=this.lists.length-1; i >= 0; i--) {
if ($.wn.foundation.element.elementContainsPoint(this.lists[i], mousePosition)) {
return false
}
}
return true
}
ListSortable.prototype.getClosestDraggableParent = function(element) {
var current = element
while (current) {
if (current.tagName === 'LI' && current.hasAttribute('draggable') ) {
return current
}
current = current.parentNode
}
return null
}
// EVENT HANDLERS
// ============================
ListSortable.prototype.onDragStart = function(ev) {
if (!this.isDragStartAllowed(ev.target)) {
return
}
ev.originalEvent.dataTransfer.effectAllowed = 'move'
ev.originalEvent.dataTransfer.setData('listsortable/elementid', this.getElementSortableId(ev.target))
ev.originalEvent.dataTransfer.setData(this.listSortableId, this.listSortableId)
// The mousemove handler is used to remove the placeholder
// when the drag is canceled with Escape button. We can't use
// the dragend for removing the placeholders because dragend
// is triggered before drop, but we need placeholder to exists
// in the drop handler.
//
// Mouse events are suppressed during the drag and drop operations,
// so we only need to handle it once (but we still must the handler
// explicitly).
$(document).on('mousemove', this.proxy(this.onDocumentMouseMove))
// The dragover handler is used to hide the placeholder when
// the mouse is outside of any known list.
$(document).on('dragover', this.proxy(this.onDocumentDragOver))
}
ListSortable.prototype.onDragOver = function(ev) {
if (!this.isSourceManagedList(ev.originalEvent)) {
return
}
var draggable = this.getClosestDraggableParent(ev.target)
if (!draggable) {
return
}
if (!this.elementIsPlaceholder(draggable) && this.lastMousePositionChanged(ev)) {
this.createPlaceholder(draggable, ev.originalEvent)
}
ev.stopPropagation()
ev.preventDefault()
ev.originalEvent.dataTransfer.dropEffect = 'move'
}
ListSortable.prototype.onDragEnter = function(ev) {
if (!this.isSourceManagedList(ev.originalEvent)) {
return
}
var draggable = this.getClosestDraggableParent(ev.target)
if (!draggable) {
return
}
if (this.elementIsPlaceholder(draggable)) {
return
}
this.createPlaceholder(draggable, ev.originalEvent)
ev.stopPropagation()
ev.preventDefault()
}
ListSortable.prototype.onDragLeave = function(ev) {
if (!this.isSourceManagedList(ev.originalEvent)) {
return
}
ev.stopPropagation()
ev.preventDefault()
}
ListSortable.prototype.onDragDrop = function(ev) {
if (!this.isSourceManagedList(ev.originalEvent)) {
return
}
var draggable = this.getClosestDraggableParent(ev.target)
if (!draggable) {
return
}
this.moveElement(draggable, ev.originalEvent)
this.removePlaceholders()
}
ListSortable.prototype.onDragEnd = function(ev) {
$(document).off('dragover', this.proxy(this.onDocumentDragOver))
}
ListSortable.prototype.onDocumentDragOver = function(ev) {
if (!this.isSourceManagedList(ev.originalEvent)) {
return
}
if (this.mouseOutsideLists(ev.originalEvent)) {
this.removePlaceholders()
return
}
}
ListSortable.prototype.onDocumentMouseMove = function(ev) {
$(document).off('mousemove', this.proxy(this.onDocumentMouseMove))
this.removePlaceholders()
}
// PLUGIN DEFINITION
// ============================
ListSortable.DEFAULTS = {
handle: null,
direction: 'vertical'
}
var old = $.fn.listSortable
$.fn.listSortable = function (option) {
var args = arguments
return this.each(function () {
var $this = $(this),
data = $this.data('oc.listSortable'),
options = $.extend({}, ListSortable.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) {
$this.data('oc.listSortable', (data = new ListSortable(this, options)))
}
if (typeof option == 'string' && data) {
if (data[option]) {
var methodArguments = Array.prototype.slice.call(args) // Clone the arguments array
methodArguments.shift()
data[option].apply(data, methodArguments)
}
}
})
}
$.fn.listSortable.Constructor = ListSortable
// LISTSORTABLE NO CONFLICT
// =================
$.fn.listSortable.noConflict = function () {
$.fn.listSortable = old
return this
}
$(document).render(function(){
$('[data-control=list-sortable]').listSortable()
})
}(window.jQuery);

View File

@@ -0,0 +1,140 @@
/*
* The loading indicator.
*
* The load indicator DIV is injected inside its container. The container should have
* the relative position (use the loading-indicator-container class for it).
*
* Used with framework.js
*
* data-load-indicator="Message" - displays a load indicator with a supplied message, the element
* must be wrapped in a `<div class="loading-indicator-container"></div>` container.
*
* JavaScript API:
*
* $('#buttons').loadIndicator({ text: 'Saving...', opaque: true }) - display the indicator in a solid (opaque) state
* $('#buttons').loadIndicator({ centered: true }) - display the indicator aligned in the center horizontally
* $('#buttons').loadIndicator({ size: small }) - display the indicator in small size
* $('#buttons').loadIndicator({ text: 'Saving...' }) - display the indicator in a transparent state
* $('#buttons').loadIndicator('hide') - display the indicator
*/
+function ($) { "use strict";
var LoadIndicator = function (element, options) {
this.$el = $(element)
this.options = options || {}
this.tally = 0
this.show()
}
LoadIndicator.prototype.hide = function() {
this.tally--
if (this.tally <= 0) {
$('div.loading-indicator', this.$el).remove()
this.$el.removeClass('in-progress')
}
}
LoadIndicator.prototype.show = function(options) {
if (options)
this.options = options
this.hide()
var indicator = $('<div class="loading-indicator"></div>')
indicator.append($('<div></div>').text(this.options.text))
indicator.append($('<span></span>'))
if (this.options.opaque !== undefined) {
indicator.addClass('is-opaque')
}
if (this.options.centered !== undefined) {
indicator.addClass('indicator-center')
}
if (this.options.size === 'small') {
indicator.addClass('size-small')
}
this.$el.prepend(indicator)
this.$el.addClass('in-progress')
this.tally++
}
LoadIndicator.prototype.destroy = function() {
this.$el.removeData('oc.loadIndicator')
this.$el = null
}
LoadIndicator.DEFAULTS = {
text: ''
}
// LOADINDICATOR PLUGIN DEFINITION
// ============================
var old = $.fn.loadIndicator
$.fn.loadIndicator = function (option) {
var args = arguments;
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.loadIndicator')
var options = $.extend({}, LoadIndicator.DEFAULTS, typeof option == 'object' && option)
if (!data) {
if (typeof option == 'string')
return;
$this.data('oc.loadIndicator', (data = new LoadIndicator(this, options)))
} else {
if (typeof option !== 'string')
data.show(options);
else {
var methodArgs = [];
for (var i=1; i<args.length; i++)
methodArgs.push(args[i])
data[option].apply(data, methodArgs)
}
}
})
}
$.fn.loadIndicator.Constructor = LoadIndicator
// LOADINDICATOR NO CONFLICT
// =================
$.fn.loadIndicator.noConflict = function () {
$.fn.loadIndicator = old
return this
}
// LOADINDICATOR DATA-API
// ==============
$(document)
.on('ajaxPromise', '[data-load-indicator]', function() {
var
indicatorContainer = $(this).closest('.loading-indicator-container'),
loadingText = $(this).data('load-indicator'),
options = {
opaque: $(this).data('load-indicator-opaque'),
centered: $(this).data('load-indicator-centered'),
size: $(this).data('load-indicator-size')
}
if (loadingText)
options.text = loadingText
indicatorContainer.loadIndicator(options)
})
.on('ajaxFail ajaxDone ajaxRedirected', '[data-load-indicator]', function() {
$(this).closest('.loading-indicator-container').loadIndicator('hide')
})
}(window.jQuery);

View File

@@ -0,0 +1,90 @@
/*
* The loading indicator for the mouse cursor.
*
* Displays the animated loading indicator following the mouse cursor.
*
* JavaScript API:
* $.wn.cursorLoadIndicator.show(event)
* $.wn.cursorLoadIndicator.hide()
*
* By default if the show() method has been called several times, the hide() method should be
* called the same number of times in order to hide the cursor. Use hide(true) to hide the
* indicator forcibly.
*
* The event parameter in the show() method is optional. If it is passed, the initial cursor position
* will be loaded from it.
*
* Require:
* - modernizr/modernizr
*/
+function ($) { "use strict";
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
var CursorLoadIndicator = function () {
if (Modernizr.touchevents)
return
this.counter = 0
this.indicator = $('<div/>').addClass('cursor-loading-indicator').addClass('hide')
$(document.body).append(this.indicator)
}
CursorLoadIndicator.prototype.show = function(event) {
if (Modernizr.touchevents)
return
this.counter++
if (this.counter > 1)
return
var self = this;
if (event !== undefined && event.clientY !== undefined) {
self.indicator.css({
left: event.clientX + 15,
top: event.clientY + 15
})
}
this.indicator.removeClass('hide')
$(window).on('mousemove.cursorLoadIndicator', function(e){
self.indicator.css({
left: e.clientX + 15,
top: e.clientY + 15,
})
})
}
CursorLoadIndicator.prototype.hide = function(force) {
if (Modernizr.touchevents)
return
this.counter--
if (force !== undefined && force)
this.counter = 0
if (this.counter <= 0) {
this.indicator.addClass('hide')
$(window).off('.cursorLoadIndicator');
}
}
$(document).ready(function() {
$.wn.cursorLoadIndicator = new CursorLoadIndicator();
})
// CURSORLOADINDICATOR DATA-API
// ==============
$(document)
.on('ajaxPromise', '[data-cursor-load-indicator]', function() {
$.wn.cursorLoadIndicator.show()
}).on('ajaxFail ajaxDone ajaxRedirected', '[data-cursor-load-indicator]', function() {
$.wn.cursorLoadIndicator.hide()
})
}(window.jQuery);

View File

@@ -0,0 +1,100 @@
/*
* The stripe loading indicator.
*
* Displays the animated loading indicator stripe at the top of the page.
*
* JavaScript API:
* $.wn.stripeLoadIndicator.show(event)
* $.wn.stripeLoadIndicator.hide()
*
* By default if the show() method has been called several times, the hide() method should be
* called the same number of times in order to hide the stripe. Use hide(true) to hide the
* indicator forcibly.
*/
+function ($) { "use strict";
if ($.wn === undefined)
$.wn = {}
if ($.oc === undefined)
$.oc = $.wn
var StripeLoadIndicator = function () {
var self = this
this.counter = 0
this.indicator = this.makeIndicator()
this.stripe = this.indicator.find('.stripe')
this.animationTimer = null
$(document).ready(function(){
$(document.body).append(self.indicator)
})
}
StripeLoadIndicator.prototype.makeIndicator = function() {
return $('<div/>')
.addClass('stripe-loading-indicator loaded')
.append($('<div />').addClass('stripe'))
.append($('<div />').addClass('stripe-loaded'))
}
StripeLoadIndicator.prototype.show = function() {
window.clearTimeout(this.animationTimer)
this.indicator.show()
this.counter++
// Restart the animation
this.stripe.after(this.stripe = this.stripe.clone()).remove()
if (this.counter > 1) {
return
}
this.indicator.removeClass('loaded')
$(document.body).addClass('loading')
}
StripeLoadIndicator.prototype.hide = function(force) {
this.counter--
if (force !== undefined && force) {
this.counter = 0
}
if (this.counter <= 0) {
this.indicator.addClass('loaded')
$(document.body).removeClass('loading')
// Stripe should be hidden using `display: none` because leaving the animated
// element in the rendering tree, even invisible, affects performance.
var self = this
this.animationTimer = window.setTimeout(function() {
self.indicator.hide()
}, 1000)
}
}
$.wn.stripeLoadIndicator = new StripeLoadIndicator()
// STRIPE LOAD INDICATOR DATA-API
// ==============
$(document)
.on('ajaxPromise', '[data-stripe-load-indicator]', function(event) {
// Prevent this event from bubbling up to a non-related data-request
// element, for example a <form> tag wrapping a <button> tag
event.stopPropagation()
$.wn.stripeLoadIndicator.show()
// This code will cover instances where the element has been removed
// from the DOM, making the resolution event below an orphan.
var $el = $(this)
$(window).one('ajaxUpdateComplete', function(){
if ($el.closest('html').length === 0)
$.wn.stripeLoadIndicator.hide()
})
}).on('ajaxFail ajaxDone ajaxRedirected', '[data-stripe-load-indicator]', function(event) {
event.stopPropagation()
$.wn.stripeLoadIndicator.hide()
})
}(window.jQuery);

View File

@@ -0,0 +1,389 @@
/*
* Popover plugin
*
* Documentation: ../docs/popover.md
*/
+function ($) { "use strict";
var Popover = function (element, options) {
var $el = this.$el = $(element);
this.options = options || {};
this.arrowSize = 15
this.docClickHandler = null
this.show()
}
Popover.prototype.hide = function() {
var e = $.Event('hiding.oc.popover', {relatedTarget: this.$el})
this.$el.trigger(e, this)
if (e.isDefaultPrevented())
return
this.$container.removeClass('in')
if (this.$overlay) this.$overlay.removeClass('in')
this.disposeControls()
$.support.transition && this.$container.hasClass('fade')
? this.$container
.one($.support.transition.end, $.proxy(this.hidePopover, this))
.emulateTransitionEnd(300)
: this.hidePopover()
}
Popover.prototype.disposeControls = function() {
if (this.$container) {
$.wn.foundation.controlUtils.disposeControls(this.$container.get(0))
}
}
Popover.prototype.hidePopover = function() {
this.$container.remove();
if (this.$overlay) this.$overlay.remove()
this.$el.removeClass('popover-highlight')
this.$el.trigger('hide.oc.popover')
this.$overlay = false
this.$container = false
this.$el.data('oc.popover', null)
$(document.body).removeClass('popover-open')
$(document).unbind('mousedown', this.docClickHandler);
$(document).off('.oc.popover')
this.docClickHandler = null
this.options.onCheckDocumentClickTarget = null
}
Popover.prototype.show = function(options) {
var self = this
/*
* Trigger the show event
*/
var e = $.Event('showing.oc.popover', { relatedTarget: this.$el })
this.$el.trigger(e, this)
if (e.isDefaultPrevented()) return
/*
* Create the popover container and overlay
*/
this.$container = $('<div />').addClass('control-popover')
if (this.options.containerClass) {
this.$container.addClass(this.options.containerClass)
}
if (this.options.useAnimation) {
this.$container.addClass('fade')
}
var $content = $('<div />').html(this.getContent())
this.$container.append($content)
if (this.options.width) {
this.$container.width(this.options.width)
}
if (this.options.modal) {
this.$overlay = $('<div />').addClass('popover-overlay')
$(document.body).append(this.$overlay)
if (this.options.highlightModalTarget) {
this.$el.addClass('popover-highlight')
this.$el.blur()
}
}
else {
this.$overlay = false
}
if (this.options.container) {
$(this.options.container).append(this.$container)
}
else {
$(document.body).append(this.$container)
}
/*
* Determine the popover position
*/
this.reposition()
$(window).on('resize', function() {
if (self.$container) {
self.reposition()
}
})
/*
* Display the popover
*/
this.$container.addClass('in')
if (this.$overlay) this.$overlay.addClass('in')
$(document.body).addClass('popover-open')
var showEvent = jQuery.Event('show.oc.popover', { relatedTarget: this.$container.get(0) })
this.$el.trigger(showEvent)
/*
* Bind events
*/
this.$container.on('close.oc.popover', function(e){
self.hide()
})
this.$container.on('click', '[data-dismiss=popover]', function(e){
self.hide()
return false
})
this.docClickHandler = $.proxy(this.onDocumentClick, this)
$(document).bind('mousedown', this.docClickHandler);
if (this.options.closeOnEsc) {
$(document).on('keyup.oc.popover', function(e){
if ($(e.target).hasClass('select2-offscreen'))
return false
if (!self.options.closeOnEsc) { // The value of the option could be changed after the popover is displayed
return false
}
if (e.key === 'Escape') {
self.hide()
return false
}
})
}
}
Popover.prototype.reposition = function() {
var
placement = this.calcPlacement(),
position = this.calcPosition(placement)
this.$container.removeClass('placement-center placement-bottom placement-top placement-left placement-right')
this.$container.css({
left: position.x,
top: position.y
}).addClass('placement-'+placement)
}
Popover.prototype.getContent = function () {
if (this.options.contentFrom) {
return $(this.options.contentFrom).html()
}
if (typeof this.options.content == 'function') {
return this.options.content.call(this.$el[0], this)
}
return this.options.content
}
Popover.prototype.calcDimensions = function() {
var
documentWidth = $(document).width(),
documentHeight = $(document).height(),
targetOffset = this.$el.offset(),
targetWidth = this.$el.outerWidth(),
targetHeight = this.$el.outerHeight()
return {
containerWidth: this.$container.outerWidth() + this.arrowSize,
containerHeight: this.$container.outerHeight() + this.arrowSize,
targetOffset: targetOffset,
targetHeight: targetHeight,
targetWidth: targetWidth,
spaceLeft: targetOffset.left,
spaceRight: documentWidth - (targetWidth + targetOffset.left),
spaceTop: targetOffset.top,
spaceBottom: documentHeight - (targetHeight + targetOffset.top),
spaceHorizontalBottom: documentHeight - targetOffset.top,
spaceVerticalRight: documentWidth - targetOffset.left,
documentWidth: documentWidth
}
}
Popover.prototype.fitsLeft = function(dimensions) {
return dimensions.spaceLeft >= dimensions.containerWidth &&
dimensions.spaceHorizontalBottom >= dimensions.containerHeight
}
Popover.prototype.fitsRight = function(dimensions) {
return dimensions.spaceRight >= dimensions.containerWidth &&
dimensions.spaceHorizontalBottom >= dimensions.containerHeight
}
Popover.prototype.fitsBottom = function(dimensions) {
return dimensions.spaceBottom >= dimensions.containerHeight &&
dimensions.spaceVerticalRight >= dimensions.containerWidth
}
Popover.prototype.fitsTop = function(dimensions) {
return dimensions.spaceTop >= dimensions.containerHeight &&
dimensions.spaceVerticalRight >= dimensions.containerWidth
}
Popover.prototype.calcPlacement = function() {
var
placement = this.options.placement,
dimensions = this.calcDimensions();
if (placement == 'center')
return placement
if (placement != 'bottom' && placement != 'top' && placement != 'left' && placement != 'right')
placement = 'bottom'
var placementFunctions = {
top: this.fitsTop,
bottom: this.fitsBottom,
left: this.fitsLeft,
right: this.fitsRight
}
if (placementFunctions[placement](dimensions))
return placement
for (var index in placementFunctions) {
if (placementFunctions[index](dimensions))
return index
}
return this.options.fallbackPlacement
}
Popover.prototype.calcPosition = function(placement) {
var
dimensions = this.calcDimensions(),
result
switch (placement) {
case 'left':
var realOffset = this.options.offsetY === undefined ? this.options.offset : this.options.offsetY
result = {x: (dimensions.targetOffset.left - dimensions.containerWidth), y: dimensions.targetOffset.top + realOffset}
break;
case 'top':
var realOffset = this.options.offsetX === undefined ? this.options.offset : this.options.offsetX
result = {x: dimensions.targetOffset.left + realOffset, y: (dimensions.targetOffset.top - dimensions.containerHeight)}
break;
case 'bottom':
var realOffset = this.options.offsetX === undefined ? this.options.offset : this.options.offsetX
result = {x: dimensions.targetOffset.left + realOffset, y: (dimensions.targetOffset.top + dimensions.targetHeight + this.arrowSize)}
break;
case 'right':
var realOffset = this.options.offsetY === undefined ? this.options.offset : this.options.offsetY
result = {x: (dimensions.targetOffset.left + dimensions.targetWidth + this.arrowSize), y: dimensions.targetOffset.top + realOffset}
break;
case 'center' :
var windowHeight = $(window).height()
result = {x: (dimensions.documentWidth/2 - dimensions.containerWidth/2), y: (windowHeight/2 - dimensions.containerHeight/2)}
if (result.y < 40)
result.y = 40
break;
}
if (!this.options.container)
return result
var
$container = $(this.options.container),
containerOffset = $container.offset()
result.x -= containerOffset.left
result.y -= containerOffset.top
return result
}
Popover.prototype.onDocumentClick = function(e) {
if (!this.options.closeOnPageClick)
return
if (this.options.onCheckDocumentClickTarget && this.options.onCheckDocumentClickTarget(e.target)) {
return
}
if ($.contains(this.$container.get(0), e.target))
return
this.hide();
}
Popover.DEFAULTS = {
placement: 'bottom',
fallbackPlacement: 'bottom',
content: '<p>Popover content<p>',
contentFrom: null,
width: false,
modal: false,
highlightModalTarget: false,
closeOnPageClick: true,
closeOnEsc: true,
container: false,
containerClass: null,
offset: 15,
useAnimation: false,
onCheckDocumentClickTarget: null
}
// POPOVER PLUGIN DEFINITION
// ============================
var old = $.fn.ocPopover
$.fn.ocPopover = function (option) {
var args = arguments;
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.popover')
var options = $.extend({}, Popover.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) {
if (typeof option == 'string')
return;
$this.data('oc.popover', (data = new Popover(this, options)))
} else {
if (typeof option != 'string')
return;
var methodArgs = [];
for (var i=1; i<args.length; i++)
methodArgs.push(args[i])
data[option].apply(data, methodArgs)
}
})
}
$.fn.ocPopover.Constructor = Popover
// POPOVER NO CONFLICT
// =================
$.fn.ocPopover.noConflict = function () {
$.fn.ocPopover = old
return this
}
// POPOVER DATA-API
// ===============
$(document).on('click', '[data-control=popover]', function(e){
$(this).ocPopover()
return false;
})
}(window.jQuery);

View File

@@ -0,0 +1,429 @@
/*
* Ajax Popup plugin
*
* Documentation: ../docs/popup.md
*
* Require:
* - bootstrap/modal
*/
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
// POPUP CLASS DEFINITION
// ============================
var Popup = function(element, options) {
this.options = options
this.$el = $(element)
this.$container = null
this.$modal = null
this.$backdrop = null
this.isOpen = false
this.isLoading = false
this.firstDiv = null
this.allowHide = true
this.$container = this.createPopupContainer()
this.$content = this.$container.find('.modal-content:first')
this.$dialog = this.$container.find('.modal-dialog:first')
this.$modal = this.$container.modal({ show: false, backdrop: false, keyboard: this.options.keyboard })
$.wn.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.initEvents()
this.init()
}
Popup.prototype = Object.create(BaseProto)
Popup.prototype.constructor = Popup
Popup.DEFAULTS = {
ajax: null,
handler: null,
keyboard: true,
extraData: {},
content: null,
size: null,
adaptiveHeight: false,
zIndex: null
}
Popup.prototype.init = function(){
var self = this
/*
* Do not allow the same popup to open twice
*/
if (self.isOpen) return
/*
* Show loading panel
*/
this.setBackdrop(true)
if (!this.options.content) {
this.setLoading(true)
}
/*
* Winter AJAX
*/
if (this.options.handler) {
this.$el.request(this.options.handler, {
data: paramToObj('data-extra-data', this.options.extraData),
success: function(data, textStatus, jqXHR) {
this.success(data, textStatus, jqXHR).done(function(){
self.setContent(data.result)
$(window).trigger('ajaxUpdateComplete', [this, data, textStatus, jqXHR])
self.triggerEvent('popupComplete') // Deprecated
self.triggerEvent('complete.oc.popup')
})
},
error: function(jqXHR, textStatus, errorThrown) {
this.error(jqXHR, textStatus, errorThrown).done(function(){
if (self.isLoading) {
self.hideLoading();
} else {
self.hide()
}
self.triggerEvent('popupError') // Deprecated
self.triggerEvent('error.oc.popup')
})
}
})
}
/*
* Regular AJAX
*/
else if (this.options.ajax) {
$.ajax({
url: this.options.ajax,
data: paramToObj('data-extra-data', this.options.extraData),
success: function(data) {
self.setContent(data)
},
cache: false
})
}
/*
* Specified content
*/
else if (this.options.content) {
var content = typeof this.options.content == 'function'
? this.options.content.call(this.$el[0], this)
: this.options.content
this.setContent(content)
}
}
Popup.prototype.initEvents = function(){
var self = this
/*
* Duplicate the popup reference on the .control-popup container
*/
this.$container.data('oc.popup', this)
/*
* Hook in to BS Modal events
*/
this.$modal.on('hide.bs.modal', function(){
self.triggerEvent('hide.oc.popup')
self.isOpen = false
self.setBackdrop(false)
})
this.$modal.on('hidden.bs.modal', function(){
self.triggerEvent('hidden.oc.popup')
$.wn.foundation.controlUtils.disposeControls(self.$container.get(0))
self.$container.remove()
$(document.body).removeClass('modal-open')
self.dispose()
})
this.$modal.on('show.bs.modal', function(){
self.isOpen = true
self.setBackdrop(true)
$(document.body).addClass('modal-open')
})
this.$modal.on('shown.bs.modal', function(){
self.triggerEvent('shown.oc.popup')
})
this.$modal.on('close.oc.popup', function(){
self.hide()
return false
})
}
Popup.prototype.dispose = function() {
this.$modal.off('hide.bs.modal')
this.$modal.off('hidden.bs.modal')
this.$modal.off('show.bs.modal')
this.$modal.off('shown.bs.modal')
this.$modal.off('close.oc.popup')
this.$el.off('dispose-control', this.proxy(this.dispose))
this.$el.removeData('oc.popup')
this.$container.removeData('oc.popup')
this.$container = null
this.$content = null
this.$dialog = null
this.$modal = null
this.$el = null
// In some cases options could contain callbacks,
// so it's better to clean them up too.
this.options = null
BaseProto.dispose.call(this)
}
Popup.prototype.createPopupContainer = function() {
var
modal = $('<div />').prop({
class: 'control-popup modal fade',
role: 'dialog',
tabindex: -1
}),
modalDialog = $('<div />').addClass('modal-dialog'),
modalContent = $('<div />').addClass('modal-content')
if (this.options.size)
modalDialog.addClass('size-' + this.options.size)
if (this.options.adaptiveHeight)
modalDialog.addClass('adaptive-height')
if (this.options.zIndex !== null)
modal.css('z-index', this.options.zIndex + 20)
return modal.append(modalDialog.append(modalContent))
}
Popup.prototype.setContent = function(contents) {
this.$content.html(contents)
this.setLoading(false)
this.show()
// Duplicate the popup object reference on to the first div
// inside the popup. Eg: $('#firstDiv').popup('hide')
this.firstDiv = this.$content.find('>div:first')
if (this.firstDiv.length > 0)
this.firstDiv.data('oc.popup', this)
var $defaultFocus = $('[default-focus]', this.$content)
if ($defaultFocus.is(":visible")) {
window.setTimeout(function() {
$defaultFocus.focus()
$defaultFocus = null
}, 300)
}
}
Popup.prototype.setBackdrop = function(val) {
if (val && !this.$backdrop) {
this.$backdrop = $('<div class="popup-backdrop fade" />')
if (this.options.zIndex !== null)
this.$backdrop.css('z-index', this.options.zIndex)
this.$backdrop.appendTo(document.body)
this.$backdrop.addClass('in')
this.$backdrop.append($('<div class="modal-content popup-loading-indicator" />'))
}
else if (!val && this.$backdrop) {
this.$backdrop.remove()
this.$backdrop = null
}
}
Popup.prototype.setLoading = function(val) {
if (!this.$backdrop)
return;
this.isLoading = val
var self = this
if (val) {
setTimeout(function(){ self.$backdrop.addClass('loading'); }, 100)
}
else {
setTimeout(function(){ self.$backdrop.removeClass('loading'); }, 100)
}
}
Popup.prototype.setShake = function() {
var self = this
this.$content.addClass('popup-shaking')
setTimeout(function() {
self.$content.removeClass('popup-shaking')
}, 1000)
}
Popup.prototype.hideLoading = function(val) {
this.setLoading(false)
// Wait for animations to complete
var self = this
setTimeout(function() { self.setBackdrop(false) }, 250)
setTimeout(function() { self.hide() }, 500)
}
Popup.prototype.triggerEvent = function(eventName, params) {
if (!params) {
params = [this.$el, this.$modal]
}
var eventObject = jQuery.Event(eventName, { relatedTarget: this.$container.get(0) })
this.$el.trigger(eventObject, params)
if (this.firstDiv) {
this.firstDiv.trigger(eventObject, params)
}
}
Popup.prototype.reload = function() {
this.init()
}
Popup.prototype.show = function() {
this.$modal.modal('show')
this.$modal.on('click.dismiss.popup', '[data-dismiss="popup"]', $.proxy(this.hide, this))
this.triggerEvent('popupShow') // Deprecated
this.triggerEvent('show.oc.popup')
// Fixes an issue where the Modal makes `position: fixed` elements relative to itself
// https://github.com/twbs/bootstrap/issues/15856
this.$dialog.css('transform', 'inherit')
}
Popup.prototype.hide = function() {
if (!this.isOpen) return
this.triggerEvent('popupHide') // Deprecated
this.triggerEvent('hide.oc.popup')
if (this.allowHide)
this.$modal.modal('hide')
// Fixes an issue where the Modal makes `position: fixed` elements relative to itself
// https://github.com/twbs/bootstrap/issues/15856
this.$dialog.css('transform', '')
}
/*
* Hide the popup without destroying it,
* you should call .hide() once finished
*/
Popup.prototype.visible = function(val) {
if (val) {
this.$modal.addClass('in')
}
else {
this.$modal.removeClass('in')
}
this.setBackdrop(val)
}
Popup.prototype.toggle = function() {
this.triggerEvent('popupToggle', [this.$modal]) // Deprecated
this.triggerEvent('toggle.oc.popup', [this.$modal])
this.$modal.modal('toggle')
}
/*
* Lock the popup from closing
*/
Popup.prototype.lock = function(val) {
this.allowHide = !val
}
// POPUP PLUGIN DEFINITION
// ============================
var old = $.fn.popup
$.fn.popup = function (option) {
var args = Array.prototype.slice.call(arguments, 1)
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.popup')
var options = $.extend({}, Popup.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.popup', (data = new Popup(this, options)))
else if (typeof option == 'string') data[option].apply(data, args)
else data.reload()
})
}
$.fn.popup.Constructor = Popup
$.popup = function (option) {
return $('<a />').popup(option)
}
// POPUP NO CONFLICT
// =================
$.fn.popup.noConflict = function () {
$.fn.popup = old
return this
}
// POPUP DATA-API
// ===============
function paramToObj(name, value) {
if (value === undefined) value = ''
if (typeof value == 'object') return value
try {
return ocJSON("{" + value + "}")
}
catch (e) {
throw new Error('Error parsing the '+name+' attribute value. '+e)
}
}
$(document).on('click.oc.popup', '[data-control="popup"]', function(event) {
event.preventDefault()
$(this).popup()
});
/*
* Only use the popup loading indicator if the handlers are an exact match.
*/
$(document)
.on('ajaxPromise', '[data-popup-load-indicator]', function(event, context) {
if ($(this).data('request') != context.handler) return
$(this).closest('.control-popup').removeClass('in').popup('setLoading', true)
})
.on('ajaxFail', '[data-popup-load-indicator]', function(event, context) {
if ($(this).data('request') != context.handler) return
$(this).closest('.control-popup').addClass('in').popup('setLoading', false).popup('setShake')
})
.on('ajaxDone', '[data-popup-load-indicator]', function(event, context) {
if ($(this).data('request') != context.handler) return
$(this).closest('.control-popup').popup('hideLoading')
})
}(window.jQuery);

View File

@@ -0,0 +1,186 @@
/*
* Select control
*
* Require:
* - modernizr/modernizr
* - select2/select2.full
*/
(function($){
/*
* Custom drop downs
*/
$(document).render(function(){
var formatSelectOption = function(state) {
var text = $('<span>').text(state.text).html()
if (!state.id) {
return text // optgroup
}
var $option = $(state.element),
iconClass = state.icon ? state.icon : $option.data('icon'),
imageSrc = state.image ? state.image : $option.data('image')
if (iconClass) {
return '<i class="select-icon '+iconClass+'"></i> ' + text
}
if (imageSrc) {
return '<img class="select-image" src="'+imageSrc+'" alt="" /> ' + text
}
return text
}
var selectOptions = {
templateResult: formatSelectOption,
templateSelection: formatSelectOption,
escapeMarkup: function(m) { return m },
width: 'style'
}
/*
* Bind custom select
*/
$('select.custom-select').each(function(){
var $element = $(this),
extraOptions = {
dropdownCssClass: '',
containerCssClass: ''
}
// Prevent duplicate loading
if ($element.data('select2') != null) {
return true; // Continue
}
$element.attr('data-disposable', 'data-disposable')
$element.one('dispose-control', function(){
if ($element.data('select2')) {
$element.select2('destroy')
}
})
if ($element.hasClass('select-no-search')) {
extraOptions.minimumResultsForSearch = Infinity
}
if ($element.hasClass('select-modifiable')) {
extraOptions.tags = true;
extraOptions.createTag = function (params) {
var term = $.trim(params.term);
if (term === '') {
return null;
}
return {
id: term,
text: term,
newTag: true
};
}
extraOptions.templateResult = function (state) {
if (!state.id) {
return state.text;
}
var icon = state.newTag ? '<i class="icon-plus"></i> ' : '';
return $(
'<span>' + icon + state.text + '</span>'
);
}
}
if ($element.hasClass('select-no-dropdown')) {
extraOptions.dropdownCssClass += ' select-no-dropdown'
extraOptions.containerCssClass += ' select-no-dropdown'
}
if ($element.hasClass('select-hide-selected')) {
extraOptions.dropdownCssClass += ' select-hide-selected'
}
/*
* Winter AJAX
*/
var source = $element.data('handler');
if (source) {
extraOptions.ajax = {
transport: function(params, success, failure) {
var $request = $element.request(source, {
data: params.data
})
$request.done(success)
$request.fail(failure)
return $request
},
processResults: function (data, params) {
var results = data.result || data.results,
options = []
delete(data.result)
if (results[0] && typeof(results[0]) === 'object') { // Pass through Select2 format
options = results
}
else { // Key-value map
for (var i in results) {
if (results.hasOwnProperty(i)) {
options.push({
id: i,
text: results[i],
})
}
}
}
data.results = options
return data
},
dataType: 'json'
}
}
var separators = $element.data('token-separators')
if (separators) {
extraOptions.tags = true
extraOptions.tokenSeparators = separators.split('|')
/*
* When the dropdown is hidden, force the first option to be selected always.
*/
if ($element.hasClass('select-no-dropdown')) {
extraOptions.selectOnClose = true
extraOptions.closeOnSelect = true
extraOptions.minimumInputLength = 1
$element.on('select2:closing', function() {
if ($('.select2-dropdown.select-no-dropdown:first .select2-results__option--highlighted').length > 0) {
$('.select2-dropdown.select-no-dropdown:first .select2-results__option--highlighted').removeClass('select2-results__option--highlighted')
$('.select2-dropdown.select-no-dropdown:first .select2-results__option:first').addClass('select2-results__option--highlighted')
}
})
}
}
var placeholder = $element.data('placeholder')
if (placeholder) {
extraOptions.placeholder = placeholder
extraOptions.allowClear = true
}
$element.select2($.extend({}, selectOptions, extraOptions))
})
})
$(document).on('disable', 'select.custom-select', function(event, status) {
if ($(this).data('select2') != null) {
$(this).select2('enable', !status)
}
})
})(jQuery);

View File

@@ -0,0 +1,432 @@
/*
* Tab control
*
* Documentation: ../docs/tab.md
*
* Require:
* - bootstrap/transition
* - bootstrap/tab
* - storm/toolbar
*/
+function ($) { "use strict";
var Tab = function (element, options) {
var $el = this.$el = $(element);
this.options = options || {}
this.$tabsContainer = $('.nav-tabs:first', $el)
this.$pagesContainer = $('.tab-content:first', $el)
this.tabId = 'tabs' + $el.parents().length + Math.round(Math.random()*1000);
if (this.options.closable !== undefined && this.options.closable !== false)
$el.attr('data-closable', '')
this.init()
}
Tab.prototype.init = function() {
var self = this;
this.options.slidable = this.options.slidable !== undefined && this.options.slidable !== false
$('> li', this.$tabsContainer).each(function(index){
self.initTab(this)
})
this.$el.on('close.oc.tab', function(ev, data){
ev.preventDefault()
var force = (data !== undefined && data.force !== undefined) ? data.force : false;
self.closeTab($(ev.target).closest('ul.nav-tabs > li, div.tab-content > div'), force)
})
this.$el.on('mousedown', "li[data-tab-id]", function (ev) {
if (ev.key === '2') {
$(ev.target).trigger('close.oc.tab');
}
})
this.$el.on('toggleCollapse.oc.tab', function(ev, data){
ev.preventDefault()
$(ev.target).closest('div.tab-content > div').toggleClass('collapsed')
})
this.$el.on('modified.oc.tab', function(ev){
ev.preventDefault()
self.modifyTab($(ev.target).closest('ul.nav-tabs > li, div.tab-content > div'))
})
this.$el.on('unmodified.oc.tab', function(ev){
ev.preventDefault()
self.unmodifyTab($(ev.target).closest('ul.nav-tabs > li, div.tab-content > div'))
})
this.$tabsContainer.on('shown.bs.tab', 'li', function(){
// self.$tabsContainer.dragScroll('fixScrollClasses')
$(window).trigger('oc.updateUi')
var tabUrl = $('> a', this).data('tabUrl')
if (!tabUrl && $(this).parent('ul').is('[data-linkable]')) {
tabUrl = $('> a', this).attr('href')
}
if (tabUrl) {
window.history.replaceState({}, 'Tab link reference', tabUrl)
}
})
if (this.options.slidable) {
this.$pagesContainer.touchwipe({
wipeRight: function(){self.prev();},
wipeLeft: function(){self.next();},
preventDefaultEvents: false,
min_move_x: 60
});
}
this.$tabsContainer.toolbar({
scrollClassContainer: this.$el
})
this.updateClasses()
if (location.hash && this.$tabsContainer.is('[data-linkable]')) {
$('li > a[href="' + location.hash + '"]', this.$tabsContainer).tab('show')
}
}
Tab.prototype.initTab = function(li) {
var
$tabs = $('>li', this.$tabsContainer),
tabIndex = $tabs.index(li),
time = new Date().getTime(),
targetId = this.tabId + '-tab-' + tabIndex + time,
$anchor = $('a', li)
$anchor
.data('target', '#'+targetId)
.attr('data-target', '#'+targetId)
.attr('data-toggle', 'tab')
if (!$anchor.attr('title'))
$anchor.attr('title', $anchor.text().trim())
// Setup the required tabs markup if it does not exist already.
if ($anchor.find('> span.title > span').length < 1) {
var html = $anchor.html()
$anchor
.html('')
.append($('<span class="title"></span>')
.append($('<span></span>').html(html)))
}
var pane = $('> .tab-pane', this.$pagesContainer).eq(tabIndex).attr('id', targetId)
if (!$('span.tab-close', li).length) {
$(li).append($('<span class="tab-close"><i>&times;</i></span>').click(function(){
$(this).trigger('close.oc.tab')
return false
}))
}
pane.data('tab', li)
this.$el.trigger('initTab.oc.tab', [{'pane': pane, 'tab': li}])
}
Tab.prototype.addTab = function(title, content, identifier, tabClass) {
var
processedTitle = this.generateTitleText(title, -1),
$link = $('<a/>').attr('href', 'javascript:;').text(processedTitle),
$li = $('<li/>'),
$pane = $('<div>').html(content).addClass('tab-pane');
$link.attr('title', title)
$li.append($link)
this.$tabsContainer.append($li)
this.$pagesContainer.append($pane)
if (tabClass !== undefined)
$link.addClass(tabClass)
if (identifier !== undefined)
$li.attr('data-tab-id', identifier)
if (this.options.paneClasses !== undefined)
$pane.addClass(this.options.paneClasses)
this.initTab($li)
$link.tab('show')
$(window).trigger('resize')
this.$tabsContainer.dragScroll('goToElement', $li)
var defaultFocus = $('[default-focus]', $pane)
if (defaultFocus.is(":visible"))
defaultFocus.focus()
this.updateClasses()
}
Tab.prototype.updateTab = function(tab, title, content) {
var tabIndex = this.findTabIndex(tab)
if (tabIndex == -1)
return
var
processedTitle = this.generateTitleText(title, -1),
$tab = $('> li', this.$tabsContainer).eq(tabIndex),
$pane = $('> div', this.$pagesContainer).eq(tabIndex),
$link = $('a', $tab)
$link.text(processedTitle).attr('title', title)
$pane.html(content)
this.initTab($tab)
this.updateClasses()
}
Tab.prototype.generateTitleText = function(title, tabIndex) {
var newTitle = title
if (this.options.titleAsFileNames)
newTitle = title.replace(/^.*[\\\/]/, '')
if (this.options.maxTitleSymbols && newTitle.length > this.options.maxTitleSymbols)
newTitle = '...'+newTitle.substring(newTitle.length - this.options.maxTitleSymbols)
return newTitle
}
Tab.prototype.closeTab = function(tab, force) {
var tabIndex = this.findTabIndex(tab)
if (tabIndex == -1)
return
var
$tab = $('> li', this.$tabsContainer).eq(tabIndex),
$pane = $('> div', this.$pagesContainer).eq(tabIndex),
isActive = $tab.hasClass('active'),
isModified = $tab.attr('data-modified') !== undefined;
if (isModified && this.options.closeConfirmation !== undefined && force !== true) {
if (!confirm(this.options.closeConfirmation))
return
}
var e = $.Event('beforeClose.oc.tab', { relatedTarget: $pane })
this.$el.trigger(e)
if (e.isDefaultPrevented())
return
$.wn.foundation.controlUtils.disposeControls($pane.get(0))
$pane.remove()
$tab.remove()
if (isActive)
$('> li > a', this.$tabsContainer).eq(tabIndex-1).tab('show')
if ($('> li > a', this.$tabsContainer).length == 0)
this.$el.trigger('afterAllClosed.oc.tab')
this.$el.trigger('closed.oc.tab', [$tab, $pane])
$(window).trigger('resize')
this.updateClasses()
}
Tab.prototype.updateClasses = function() {
if (this.$tabsContainer.children().length > 0)
this.$el.addClass('has-tabs')
else
this.$el.removeClass('has-tabs')
}
Tab.prototype.modifyTab = function(tab) {
var tabIndex = this.findTabIndex(tab)
if (tabIndex == -1)
return
$('> li', this.$tabsContainer).eq(tabIndex).attr('data-modified', '')
$('> div', this.$pagesContainer).eq(tabIndex).attr('data-modified', '')
}
Tab.prototype.unmodifyTab = function(tab) {
var tabIndex = this.findTabIndex(tab)
if (tabIndex == -1)
return
$('> li', this.$tabsContainer).eq(tabIndex).removeAttr('data-modified')
$('> div', this.$pagesContainer).eq(tabIndex).removeAttr('data-modified')
}
Tab.prototype.findTabIndex = function(tab) {
var tabToFind = tab
if (tab === undefined)
tabToFind = $('li.active', this.$tabsContainer)
var tabParent = this.$pagesContainer
if ($(tabToFind).parent().hasClass('nav-tabs'))
tabParent = this.$tabsContainer
return tabParent.children().index($(tabToFind))
}
Tab.prototype.findTabFromPane = function(pane) {
var id = '#' + $(pane).attr('id'),
tab = $('[data-target="' + id + '"]', this.$tabsContainer)
return tab
}
Tab.prototype.findPaneFromTab = function(tab) {
var id = $(tab).find('> a').data('target'),
pane = this.$pagesContainer.find(id)
return pane
}
Tab.prototype.goTo = function(identifier) {
var $tab = $('[data-tab-id="'+identifier+'" ]', this.$tabsContainer)
if ($tab.length == 0)
return false
var tabIndex = this.findTabIndex($tab)
if (tabIndex == -1)
return false
this.goToIndex(tabIndex)
this.$tabsContainer.dragScroll('goToElement', $tab)
return true
}
Tab.prototype.goToPane = function(pane) {
var $pane = $(pane),
$tab = this.findTabFromPane($pane)
if ($pane.length == 0)
return
$pane.removeClass('collapsed')
var tabIndex = this.findTabIndex($pane)
if (tabIndex == -1)
return false
this.goToIndex(tabIndex)
if ($tab.length > 0)
this.$tabsContainer.dragScroll('goToElement', $tab)
return true
}
Tab.prototype.goToElement = function(element) {
return this.goToPane(element.closest('.tab-pane'))
}
Tab.prototype.findByIdentifier = function(identifier) {
return $('[data-tab-id="'+identifier+'" ]', this.$tabsContainer);
}
Tab.prototype.updateIdentifier = function(tab, identifier) {
var index = this.findTabIndex(tab)
if (index == -1)
return
$('> li', this.$tabsContainer).eq(index).attr('data-tab-id', identifier)
}
Tab.prototype.updateTitle = function(tab, title) {
var index = this.findTabIndex(tab)
if (index == -1)
return
var processedTitle = this.generateTitleText(title, index),
$link = $('> li > a span.title', this.$tabsContainer).eq(index)
$link.attr('title', title)
$link.text(processedTitle)
}
Tab.prototype.goToIndex = function(index) {
$('> li > a', this.$tabsContainer).eq(index).tab('show')
}
Tab.prototype.prev = function() {
var tabIndex = this.findTabIndex()
if (tabIndex <= 0)
return
this.goToIndex(tabIndex-1)
}
Tab.prototype.next = function() {
var tabIndex = this.findTabIndex()
if (tabIndex == -1)
return
this.goToIndex(tabIndex+1)
}
Tab.DEFAULTS = {
}
// TAB PLUGIN DEFINITION
// ============================
var old = $.fn.ocTab
$.fn.ocTab = function (option) {
var args = arguments;
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.tab')
var options = $.extend({}, Tab.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.tab', (data = new Tab(this, options)))
if (typeof option == 'string') {
var methodArgs = [];
for (var i=1; i<args.length; i++)
methodArgs.push(args[i])
data[option].apply(data, methodArgs)
}
})
}
$.fn.ocTab.Constructor = Tab
// TAB NO CONFLICT
// =================
$.fn.ocTab.noConflict = function () {
$.fn.ocTab = old
return this
}
// TAB DATA-API
// ============
$(document).render(function(){
$('[data-control=tab]').ocTab()
})
/*
* Detect invalid fields, focus the tab
*/
$(window).on('ajaxInvalidField', function(event, element, name, messages, isFirst){
if (!isFirst) return
event.preventDefault()
var $el = $(element)
$el.closest('[data-control=tab]').ocTab('goToElement', $el)
$el.focus()
})
}(window.jQuery);

View File

@@ -0,0 +1,117 @@
/*
* Toolbar control.
*
* Makes toolbars drag/scrollable.
*
* Data attributes:
* - data-control="toolbar" - enables the toolbar plugin
* - data-no-drag-support="true" - disables the drag support for the toolbar, leaving only the mouse wheel support
* - data-use-native-drag="true" - if native CSS is enabled via "mobile" on the HTML tag, false by default
*
* JavaScript API:
* $('#toolbar').toolbar()
*
* Require:
* - storm/drag.scroll
*/
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var Toolbar = function (element, options) {
var
$el = this.$el = $(element),
$toolbar = $el.closest('.control-toolbar')
$.wn.foundation.controlUtils.markDisposable(element)
this.$toolbar = $toolbar
this.options = options || {};
var noDragSupport = options.noDragSupport !== undefined && options.noDragSupport
Base.call(this)
var scrollClassContainer = options.scrollClassContainer !== undefined
? options.scrollClassContainer
: $el.parent()
if (this.options.useNativeDrag) {
$el.addClass('is-native-drag')
}
$el.dragScroll({
scrollClassContainer: scrollClassContainer,
useDrag: !noDragSupport,
useNative: this.options.useNativeDrag
})
$('.form-control.growable', $toolbar).on('focus.toolbar', function(){
update()
})
$('.form-control.growable', $toolbar).on('blur.toolbar', function(){
update()
})
this.$el.one('dispose-control', this.proxy(this.dispose))
function update() {
$(window).trigger('resize')
}
}
Toolbar.prototype = Object.create(BaseProto)
Toolbar.prototype.constructor = Toolbar
Toolbar.prototype.dispose = function() {
this.$el.off('dispose-control', this.proxy(this.dispose))
$('.form-control.growable', this.$toolbar).off('.toolbar')
this.$el.dragScroll('dispose')
this.$el.removeData('oc.toolbar')
this.$el = null
BaseProto.dispose.call(this)
}
Toolbar.DEFAULTS = {
useNativeDrag: false
}
// TOOLBAR PLUGIN DEFINITION
// ============================
var old = $.fn.toolbar
$.fn.toolbar = function (option) {
var args = Array.prototype.slice.call(arguments, 1)
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.toolbar')
var options = $.extend({}, Toolbar.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.toolbar', (data = new Toolbar(this, options)))
if (typeof option == 'string') data[option].apply(data, args)
})
}
$.fn.toolbar.Constructor = Toolbar
// TOOLBAR NO CONFLICT
// =================
$.fn.toolbar.noConflict = function () {
$.fn.toolbar = old
return this
}
// TOOLBAR DATA-API
// ===============
$(document).on('render', function(){
$('[data-control=toolbar]').toolbar()
})
}(window.jQuery);

View File

@@ -0,0 +1,25 @@
/*
* Implement the tooltip control automatically
*
* Usage:
*
* <a
* href="javascript:;"
* data-toggle="tooltip"
* data-placement="left"
* title="Tooltip content">
* Some link
* </a>
*
* Require:
* - bootstrap/transition
* - bootstrap/tooltip
*/
(function($){
$(document).render(function(){
$('[data-control="tooltip"], [data-toggle="tooltip"]').tooltip()
})
})(jQuery);