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,34 @@
# Winter Storm UI
Welcome to the client-side framework designed exclusively for the Winter CMS back-end area, referred to as *Winter Storm UI*. The library is quite large as it has many features and it is not really intended to be used outside of Winter.
## Design consideration
Each LESS library should always include the `global.less` to ensure all mixins and variables are available.
Compiling JavaScript depends on Winter's asset combiner as the `=require` directive was invented here to emulate the LESS `@import` functionality.
## UI Components
Components are a mixture of CSS and JavaScript (Controls), or can be solely style-based (Styles) or solely script-based (Scripts).
Each component has a *strong name*, for example the loading indicator has the name `loader`. For complex components, they can be broken in to child components, for example `loader.stripe`. Not all child components can be used independently of their parents, but this is certainly possible and a nice idea.
> *Note*: Documentation for each component can be found in the **docs/** directory.
## Naming conventions
In most cases a control will be styled in CSS with the prefix `control-something` and the JavaScript is applied using `data-control="something"`. This allows a rendering as a styled control only, without the JavaScript and vice versa.
<div class="control-list" data-control="list">...</div>
The appearance of a control can be modified using additional CSS classes. These modifiers should be prefixed with the control name or the word `is` if the modification is binary (a boolean). For example:
<div class="control-list list-purple is-sortable"></div>
The above uses two modifiers; one to make it purple and one to declare that it is sortable. In the above example, the class `is-purple` is not recommended because it is a variable attribute as opposed to a binary one. Here are some common words used for attributes and their meanings:
- **flush**: The control will use no margin, padding and/or border to the controls or containers surrounding it. Eg: `list-flush`
- **inset**: The control will use a negative margin on the left or right side to negate a padded container. Eg: `list-inset`
- **offset**: The control will use a positive margin or padding on the left or right to assist a container with padding. Eg: `list-offset`
- **padded**: The control will use padding all around. Eg: `list-padded`

View File

@@ -0,0 +1,19 @@
# Autocomplete
### Autocomplete
Autocomplete control.
<input
class="form-control"
placeholder="Search for something or else"
data-control="autocomplete"
data-source="something: 'Something', else: 'Else'" />
## JavaScript API
```js
$('input').autocomplete({
source: { something: 'Something', else: 'Else' }
})
```

View File

@@ -0,0 +1,13 @@
Display a breadcrumb on the page.
# Example
<div style="padding: 20px">
<div class="control-breadcrumb">
<ul>
<li><a href="#">Dash Board</a></li>
<li><a href="#">Blog Posts</a></li>
<li>Edit Post</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,63 @@
# Button
## Buttons
### Button tags
Use the button classes on an `<a>`, `<button>`, or `<input>` element.
<a class="btn btn-default" href="#" role="button">Link</a>
<button class="btn btn-default" type="submit">Button</button>
<input class="btn btn-default" type="button" value="Input">
<input class="btn btn-default" type="submit" value="Submit">
### Options
Use any of the available button classes to quickly create a styled button.
<!-- Standard button -->
<button type="button" class="btn btn-default">Default</button>
<!-- Provides extra visual weight and identifies the primary action in a set of buttons -->
<button type="button" class="btn btn-primary">Primary</button>
<!-- Indicates a successful or positive action -->
<button type="button" class="btn btn-success">Success</button>
<!-- Contextual button for informational alert messages -->
<button type="button" class="btn btn-info">Info</button>
<!-- Indicates caution should be taken with this action -->
<button type="button" class="btn btn-warning">Warning</button>
<!-- Indicates a dangerous or potentially negative action -->
<button type="button" class="btn btn-danger">Danger</button>
<!-- Deemphasize a button by making it look like a link while maintaining button behavior -->
<button type="button" class="btn btn-link">Link</button>
### Sizes
Fancy larger or smaller buttons? Add `.btn-lg`, `.btn-sm`, or `.btn-xs` for additional sizes.
<p>
<button type="button" class="btn btn-primary btn-lg">Large button</button>
<button type="button" class="btn btn-default btn-lg">Large button</button>
</p>
<p>
<button type="button" class="btn btn-primary">Default button</button>
<button type="button" class="btn btn-default">Default button</button>
</p>
<p>
<button type="button" class="btn btn-primary btn-sm">Small button</button>
<button type="button" class="btn btn-default btn-sm">Small button</button>
</p>
<p>
<button type="button" class="btn btn-primary btn-xs">Extra small button</button>
<button type="button" class="btn btn-default btn-xs">Extra small button</button>
</p>
Create block level buttons—those that span the full width of a parent— by adding .btn-block.
<button type="button" class="btn btn-primary btn-lg btn-block">Block level button</button>
<button type="button" class="btn btn-default btn-lg btn-block">Block level button</button>

View File

@@ -0,0 +1,66 @@
# Callout
### Callout
Displays a detailed message to the user, also allowing it to be dismissed.
<div class="callout fade in callout-warning">
<button
type="button"
class="close"
data-dismiss="callout"
aria-hidden="true">&times;</button>
<div class="header">
<i class="icon-warning"></i>
<h3>Warning warning</h3>
<p>My arms are flailing wildly</p>
</div>
<div class="content">
<p>Insert coin(s) to begin play</p>
</div>
</div>
### No sub-header
Include the `no-subheader` class to omit the sub heading.
<div class="callout fade in callout-info no-subheader">
<div class="header">
<i class="icon-info"></i>
<h3>Incoming unicorn</h3>
</div>
</div>
### No icon
Include the `no-icon` class to omit the icon.
<div class="callout fade in callout-danger no-icon">
<div class="header">
<h3>There was a hull breach</h3>
<ul>
<li>Get to the chopper</li>
</ul>
</div>
</div>
### No header
<div class="callout fade in callout-success">
<div class="content">
<p>Something good happened</p>
<ul>
<li>You found a pony</li>
</ul>
</div>
</div>
### Data attributes:
- data-dismiss="callout" - when assigned to an element, the callout hides on click
## JavaScript API
### Events
- close.oc.callout - triggered when the callout is closed

View File

@@ -0,0 +1,127 @@
# Chart
<a name="pie-chart" class="anchor" href="#pie-chart"></a>
## Pie chart
The pie chart outputs information as a circle diagram, with optional label in the center. Example markup:
<div
class="control-chart centered wrap-legend"
data-control="chart-pie"
data-size="200"
data-center-text="100">
<ul>
<li>Label 1 <span>100</span></li>
<li>Label 2 <span>100</span></li>
<li>Label 3 <span>100</span></li>
</ul>
</div>
![image](https://github.com/wintercms/docs/blob/main/images/traffic-sources.png?raw=true) {.img-responsive .frame}
<a name="line-chart" class="anchor" href="#line-chart"></a>
## Line chart
The next example shows a line chart markup. Data sets are defined with the SPAN elements inside the chart element.
<div
data-control="chart-line"
data-time-mode="weeks"
style="height: 200px"
data-chart-options="xaxis: {mode: 'time'}">
<span
data-chart="dataset"
data-set-color="#008dc9"
data-set-data="[1477857082000, 400], [1477943482000, 380], [1478029882000, 340], [1478116282000, 540], [1478202682000, 440], [1478289082000, 360], [1478375482000, 220]"
data-set-name="Visits">
</span>
</div>
![image](https://github.com/wintercms/docs/blob/main/images/line-chart.png?raw=true) {.img-responsive .frame}
<a name="bar-chart" class="anchor" href="#bar-chart"></a>
## Bar chart
The next example shows a bar chart markup. The **wrap-legend** class is optional, it manages the legend layout. The **data-height** and **data-full-width** attributes are optional as well.
<div
class="control-chart wrap-legend"
data-control="chart-bar"
data-height="100"
data-full-width="1">
<ul>
<li>Label 1 <span>100</span></li>
<li>Label 2 <span>100</span></li>
<li>Label 3 <span>100</span></li>
</ul>
</div>
![image](https://github.com/wintercms/docs/blob/main/images/bar-chart.png?raw=true) {.img-responsive .frame}
# Example
<div
class="control-chart centered wrap-legend"
data-control="chart-pie"
data-size="200"
data-center-text="100">
<ul>
<li>Label 1 <span>100</span></li>
<li>Label 2 <span>100</span></li>
<li>Label 3 <span>100</span></li>
</ul>
</div>
<div
class="control-chart wrap-legend"
data-control="chart-bar"
data-height="100"
data-full-width="1">
<ul>
<li>Label 1 <span>100</span></li>
<li>Label 2 <span>100</span></li>
<li>Label 3 <span>100</span></li>
</ul>
</div>
<a name="bar-chart" class="anchor" href="#bar-chart"></a>
## Status list
A list of statuses and values
# Example
<div class="control-status-list">
<ul>
<li>
<span class="status-icon success"><i class="icon-check"></i></span>
<span class="status-text success">Software is up to date</span>
<a href="#" class="status-label link">Update</a>
</li>
<li>
<span class="status-icon warning"><i class="icon-exclamation"></i></span>
<span class="status-text warning">Some issues need attention</span>
<a href="#" class="status-label link">View</a>
</li>
<li>
<span class="status-icon"><i class="icon-info"></i></span>
<span class="status-text">System build</span>
<span class="status-label primary">313</span>
</li>
<li>
<span class="status-icon"><i class="icon-info"></i></span>
<span class="status-text">Event log items</span>
<span class="status-label primary">200</span>
</li>
<li>
<span class="status-icon"><i class="icon-info"></i></span>
<span class="status-text">Online since</span>
<span class="status-label link">4th April 2014</span>
</li>
</ul>
</div>

View File

@@ -0,0 +1,102 @@
# Checkbox
### Checkbox
Allows a user to select from a small set of binary options.
<div class="checkbox custom-checkbox">
<input name="checkbox" value="1" type="checkbox" id="checkbox1" />
<label for="checkbox1">Checkbox</label>
</div>
### Checkbox lists
Allows a user to select from a list of binary options.
<div class="form-group checkboxlist-field">
<label>Checkbox list (hard-coded) example</label>
<div class="field-checkboxlist">
<!-- Quick selection (start) -->
<div class="checkboxlist-controls">
<div>
<a href="javascript:;" data-field-checkboxlist-all><i class="icon-check-square"></i> <?= e(trans('backend::lang.form.select_all')) ?></a>
</div>
<div>
<a href="javascript:;" data-field-checkboxlist-none><i class="icon-eraser"></i> <?= e(trans('backend::lang.form.select_none')) ?></a>
</div>
</div>
<!-- Quick selection (end) -->
<div class="field-checkboxlist-container">
<p class="help-block before-field">What cars would you like in your garage?</p>
<div class="checkbox custom-checkbox" tabindex="0">
<input id="checkbox-example1" name="checkbox" value="1" type="checkbox" checked="checked" aria-checked="true" />
<label for="checkbox-example1"> Dodge Viper </label>
<p class="help-block">Do not send new comment notifications.</p>
</div>
<div class="checkbox custom-checkbox" tabindex="0">
<input id="checkbox-example2" name="checkbox" value="2" type="checkbox" aria-checked="false" />
<label for="checkbox-example2"> GM Corvette </label>
<p class="help-block">Send new comment notifications only to post author.</p>
</div>
<div class="checkbox custom-checkbox" tabindex="0">
<input id="checkbox-example3" name="checkbox" value="3" type="checkbox" aria-checked="mixed" />
<label for="checkbox-example3"> Porsche Boxter </label>
<p class="help-block">Notify all users who have permissions to receive blog notifications.</p>
</div>
</div>
</div>
</div>
### Indeterminate checkboxes
<div class="checkbox custom-checkbox is-indeterminate">
<input name="checkbox" value="1" type="checkbox" id="checkbox1" data-checked="1" />
<label for="checkbox1">Checkbox</label>
</div>
The `data-checked` attribute may have one of three values: 0 (off), 1 (indeterminate) or 2 (on).
### Radio
<div class="radio custom-radio">
<input name="radio" value="1" type="radio" id="radio_1" />
<label for="radio_1">Paris</label>
</div>
<div class="radio custom-radio">
<input checked="checked" name="radio" value="2" type="radio" id="radio_2" />
<label for="radio_2">Dubai</label>
</div>
<div class="radio custom-radio">
<input name="radio" value="3" type="radio" id="radio_3" />
<label for="radio_3">New Zealand</label>
</div>
### Slider
<label class="custom-switch">
<input type="checkbox" />
<span><span>On</span><span>Off</span></span>
<a class="slide-button"></a>
</label>
### Balloon selector
<div data-control="balloon-selector" class="control-balloon-selector">
<ul>
<li data-value="1" class="active">One</li>
<li data-value="2">Two</li>
<li data-value="3">Three</li>
</ul>
<input type="hidden" name="balloonValue" value="1" />
</div>
If you don't define `data-control="balloon-selector"` then the control will act as a static list of labels.
<div class="control-balloon-selector">
<ul>
<li>Monday</li>
<li>Tuesday</li>
<li>Happy days!</li>
</ul>
</div>

View File

@@ -0,0 +1,102 @@
# Date Pickers
Renders a date picker, time picker, or both. The input associated to each control acts as a facade, the final value is stored in an underlying hidden input, called a data locker.
## Examples
### Date Picker
<div data-control="datepicker">
<!-- Date -->
<input
type="text"
class="form-control"
placeholder="Select a date"
data-datepicker />
<!-- Data locker -->
<input
type="hidden"
name="my_date"
data-datetime-value
/>
</div>
### Time Picker
<div data-control="datepicker">
<!-- Time -->
<input
type="text"
class="form-control"
placeholder="Select a time"
data-timepicker />
<!-- Data locker -->
<input
type="hidden"
name="my_date"
data-datetime-value
/>
</div>
### Date & Time Picker
<div data-control="datepicker">
<div class="row">
<div class="col-md-6">
<!-- Date -->
<input
type="text"
class="form-control"
placeholder="Select a date"
data-datepicker />
</div>
<div class="col-md-6">
<!-- Time -->
<input
type="text"
class="form-control"
placeholder="Select a time"
data-timepicker />
</div>
</div>
<!-- Data locker -->
<input
type="hidden"
name="my_date"
data-datetime-value
/>
</div>
## Locale and timezone handling
The date picker handles timezone and locale preferences automatically. Locale preferences will provide the date format for the region. The timezone setting is used to convert the chosen value to a uniform timezone, commonly UTC. These features are not enabled by default and require adding `<meta />` tags to the page.
```html
<meta name="app-timezone" content="UTC">
<meta name="backend-timezone" content="Australia/Sydney">
<meta name="backend-locale" content="en-au">
```
When a date is selected, it will be converted from the `backend-timezone` to the `app-timezone` for normalized storage.
> **Note**: Locale values are supplied by the Moment.js library.
## Supported data attributes
- data-control="datepicker" - enables the plugin on an element
- data-format="YYYY-MM-DD" - display format
- data-min-date="value" - minimum date to allow
- data-max-date="value" - maximum date to allow
- data-year-range="10" - range of years to display
## JavaScript API
```js
$('div#datepicker').datePicker({
format: 'YYYY-MM-DD',
yearRange: 10
})
```

View File

@@ -0,0 +1,27 @@
# Drag.Scroll
Allows the elements with `overflow: hidden` to be dragged.
### Example
Drag the area above left-to-right.
<div id="scrollExample">
<div class="scroll-stripes-example"></div>
</div>
<style>
#scrollExample {
width: 100%; height: 50px; overflow: hidden;
}
.scroll-stripes-example {
height: 50px; width: 5000px;
background-image: linear-gradient(90deg, gray, white, gray);
background-size: 500px 50px;
}
</style>
<script>
$('#scrollExample').dragScroll();
</script>

View File

@@ -0,0 +1,108 @@
# Drag.Sort
Allows the dragging and sorting of lists.
### Example
Sort the buttons
<ol id="sortExample">
<li><a class="btn btn-sm btn-default">First</a></li>
<li><a class="btn btn-sm btn-primary">Second</a></li>
<li><a class="btn btn-sm btn-success">Third</a></li>
</ol>
<script>
$('#sortExample').sortable()
</script>
<style>
body.dragging, body.dragging * {
cursor: move !important
}
.dragged {
position: absolute; opacity: 0.5; z-index: 2000;
}
#sortExample li.placeholder {
position: relative;
}
</style>
## JavaScript API
The `sortable()` method must be invoked on valid containers, meaning they must match the containerSelector option.
`.sortable('enable')`
Enable all instantiated sortables in the set of matched elements
`.sortable('disable')`
Disable all instantiated sortables in the set of matched elements
`.sortable('refresh')`
Reset all cached element dimensions
`.sortable('destroy')`
Remove the sortable plugin from the set of matched elements
`.sortable('serialize')`
Serialize all selected containers. Returns a jQuery object . Use .get() to retrieve the array, if needed.
### Supported options
- `useAnimation`: Use animation when an item is removed or inserted into the tree.
- `usePlaceholderClone`: Placeholder should be a clone of the item being dragged.
- `afterMove`: This is executed after the placeholder has been moved. $closestItemOrContainer contains the closest item, the placeholder has been put at or the closest empty Container, the placeholder has been appended to.
- `containerPath`: The exact css path between the container and its items, e.g. "> tbody"
- `containerSelector`: The css selector of the containers
- `distance`: Distance the mouse has to travel to start dragging
- `delay`: Time in milliseconds after mousedown until dragging should start. This option can be used to prevent unwanted drags when clicking on an element.
- `handle`: The css selector of the drag handle
- `itemPath`: The exact css path between the item and its subcontainers. It should only match the immediate items of a container. No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div"
- `itemSelector`: The css selector of the items
- `bodyClass`: The class given to "body" while an item is being dragged
- `draggedClass`: The class giving to an item while being dragged
- `isValidTarget`: Check if the dragged item may be inside the container. Use with care, since the search for a valid container entails a depth first search and may be quite expensive.
- `onCancel`: Executed before onDrop if placeholder is detached. This happens if pullPlaceholder is set to false and the drop occurs outside a container.
- `onDrag`: Executed at the beginning of a mouse move event. The Placeholder has not been moved yet.
- `onDragStart`: Called after the drag has been started, that is the mouse button is being held down and the mouse is moving. The container is the closest initialized container. Therefore it might not be the container, that actually contains the item.
- `onDrop`: Called when the mouse button is being released
- `onMousedown`: Called on mousedown. If falsy value is returned, the dragging will not start. Ignore if element clicked is input, select or textarea
- `placeholderClass`: The class of the placeholder (must match placeholder option markup)
- `placeholder`: Template for the placeholder. Can be any valid jQuery input e.g. a string, a DOM element. The placeholder must have the class "placeholder"
- `pullPlaceholder`: If true, the position of the placeholder is calculated on every mousemove. If false, it is only calculated when the mouse is above a container.
- `serialize`: Specifies serialization of the container group. The pair $parent/$children is either container/items or item/subcontainers.
- `tolerance`: Set tolerance while dragging. Positive values decrease sensitivity, negative values increase it.
### Supported options (container specific)
- `drag`: If true, items can be dragged from this container
- `drop`: If true, items can be droped onto this container
- `exclude`: Exclude items from being draggable, if the selector matches the item
- `nested`: If true, search for nested containers within an item.If you nest containers, either the original selector with which you call the plugin must only match the top containers, or you need to specify a group (see the bootstrap nav example)
- `vertical`: If true, the items are assumed to be arranged vertically

View File

@@ -0,0 +1,52 @@
# Drag.Value
Allows the dragging of elements that result in a custom value when dropped.
<p>
<input placeholder="Drag a button below to me" class="form-control" />
</p>
<button
class="btn btn-default"
data-control="dragvalue"
data-text-value="Winter">
Drop "Foo"
</button>
<button
class="btn btn-default"
data-control="dragvalue"
data-text-value="CMS">
Drop "Bar"
</button>
### Clickable
You can make elements clickable from another input by defining `data-drag-click="true"`.
<p>
<input placeholder="Click on me first, then click a label below" class="form-control" />
</p>
<div class="control-balloon-selector">
<ul>
<li
data-control="dragvalue"
data-text-value="Richie"
data-drag-click="true">
Monday
</li>
<li
data-control="dragvalue"
data-text-value="Potsie"
data-drag-click="true">
Tuesday
</li>
<li
data-control="dragvalue"
data-text-value="The Fonz"
data-drag-click="true">
Happy days!
</li>
</ul>
</div>

View File

@@ -0,0 +1,43 @@
Customized dropdown menu
### Small dropdown
<div class="dropdown">
<a href="#" data-toggle="dropdown" class="btn btn-primary wn-icon-plus">Add small</a>
<ul class="dropdown-menu" role="menu" data-dropdown-title="Add something small">
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-folder">Group</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-copy">Page</a></li>
</ul>
</div>
### Drop "up"
Add the `dropup` class to the dropdown container and the dropdown will appear in an upward direction.
<div class="dropdown dropup">
...
</div>
### Large dropdown
<div class="dropdown">
<a href="#" data-toggle="dropdown" class="btn btn-primary wn-icon-plus">Add large</a>
<ul class="dropdown-menu" role="menu" data-dropdown-title="Add something large">
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-folder">Group</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-copy">Page</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-briefcase">Briefcase</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-link">Link</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-tag">Tag</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-search-minus">Zoom out</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-briefcase">Briefcase</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-link">Link</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-tag">Tag</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-search-minus">Zoom out</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-briefcase">Briefcase</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-link">Link</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-tag">Tag</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" class="wn-icon-search-minus">Zoom out</a></li>
</ul>
</div>

View File

@@ -0,0 +1,54 @@
# Inspector
## Dependencies
- Popover
# Example
<div id="filterExample" class="control-filter" data-control="filterwidget">
<!-- Group -->
<a href="javascript:;" class="filter-scope" data-scope-name="categories">
<span class="filter-label">Categories:</span>
<span class="filter-setting">all</span>
</a>
<!-- Group -->
<a href="javascript:;" class="filter-scope active" data-scope-name="statuses">
<span class="filter-label">Statuses:</span>
<span class="filter-setting">2</span>
</a>
<!-- Checkbox -->
<div class="filter-scope checkbox custom-checkbox" data-scope-name="showActive">
<input type="checkbox" id="chkActive" />
<label for="chkActive">Show active</label>
</div>
</div>
<script>
$('#filterExample').data('filterScopes', {
categories: {
available: [
{ id: 1, name: 'Announcements' },
{ id: 2, name: 'Architecture' },
{ id: 3, name: 'Products' },
{ id: 4, name: 'Services' },
{ id: 5, name: 'Clients' }
]
},
statuses: {
available: [
{ id: 1, name: 'Deleted' },
{ id: 2, name: 'Deployed' },
{ id: 3, name: 'Detailed' }
],
active: [
{ id: 4, name: 'Published' },
{ id: 5, name: 'Draft' }
]
}
})
</script>

View File

@@ -0,0 +1,269 @@
Provides flags of various descriptions using [flag-icon-css](https://github.com/lipis/flag-icon-css).
*Class 'flag-icon flag-icon-{@country}' was merged into flag-{@country}, to support Winter CMS standards.*
## Usage Example
```html
<i class="flag-ad"></i>
<i class="flag-ae"></i>
<i class="flag-af"></i>
<i class="flag-ag"></i>
<i class="flag-ai"></i>
<i class="flag-al"></i>
<i class="flag-am"></i>
<i class="flag-ao"></i>
<i class="flag-aq"></i>
<i class="flag-ar"></i>
<i class="flag-as"></i>
<i class="flag-at"></i>
<i class="flag-au"></i>
<i class="flag-aw"></i>
<i class="flag-ax"></i>
<i class="flag-az"></i>
<i class="flag-ba"></i>
<i class="flag-bb"></i>
<i class="flag-bd"></i>
<i class="flag-be"></i>
<i class="flag-bf"></i>
<i class="flag-bg"></i>
<i class="flag-bh"></i>
<i class="flag-bi"></i>
<i class="flag-bj"></i>
<i class="flag-bl"></i>
<i class="flag-bm"></i>
<i class="flag-bn"></i>
<i class="flag-bo"></i>
<i class="flag-bq"></i>
<i class="flag-br"></i>
<i class="flag-bs"></i>
<i class="flag-bt"></i>
<i class="flag-bv"></i>
<i class="flag-bw"></i>
<i class="flag-by"></i>
<i class="flag-bz"></i>
<i class="flag-ca"></i>
<i class="flag-cc"></i>
<i class="flag-cd"></i>
<i class="flag-cf"></i>
<i class="flag-cg"></i>
<i class="flag-ch"></i>
<i class="flag-ci"></i>
<i class="flag-ck"></i>
<i class="flag-cl"></i>
<i class="flag-cm"></i>
<i class="flag-cn"></i>
<i class="flag-co"></i>
<i class="flag-cr"></i>
<i class="flag-cu"></i>
<i class="flag-cv"></i>
<i class="flag-cw"></i>
<i class="flag-cx"></i>
<i class="flag-cy"></i>
<i class="flag-cz"></i>
<i class="flag-de"></i>
<i class="flag-dj"></i>
<i class="flag-dk"></i>
<i class="flag-dm"></i>
<i class="flag-do"></i>
<i class="flag-dz"></i>
<i class="flag-ec"></i>
<i class="flag-ee"></i>
<i class="flag-eg"></i>
<i class="flag-eh"></i>
<i class="flag-er"></i>
<i class="flag-es"></i>
<i class="flag-et"></i>
<i class="flag-fi"></i>
<i class="flag-fj"></i>
<i class="flag-fk"></i>
<i class="flag-fm"></i>
<i class="flag-fo"></i>
<i class="flag-fr"></i>
<i class="flag-ga"></i>
<i class="flag-gb"></i>
<i class="flag-gd"></i>
<i class="flag-ge"></i>
<i class="flag-gf"></i>
<i class="flag-gg"></i>
<i class="flag-gh"></i>
<i class="flag-gi"></i>
<i class="flag-gl"></i>
<i class="flag-gm"></i>
<i class="flag-gn"></i>
<i class="flag-gp"></i>
<i class="flag-gq"></i>
<i class="flag-gr"></i>
<i class="flag-gs"></i>
<i class="flag-gt"></i>
<i class="flag-gu"></i>
<i class="flag-gw"></i>
<i class="flag-gy"></i>
<i class="flag-hk"></i>
<i class="flag-hm"></i>
<i class="flag-hn"></i>
<i class="flag-hr"></i>
<i class="flag-ht"></i>
<i class="flag-hu"></i>
<i class="flag-id"></i>
<i class="flag-ie"></i>
<i class="flag-il"></i>
<i class="flag-im"></i>
<i class="flag-in"></i>
<i class="flag-io"></i>
<i class="flag-iq"></i>
<i class="flag-ir"></i>
<i class="flag-is"></i>
<i class="flag-it"></i>
<i class="flag-je"></i>
<i class="flag-jm"></i>
<i class="flag-jo"></i>
<i class="flag-jp"></i>
<i class="flag-ke"></i>
<i class="flag-kg"></i>
<i class="flag-kh"></i>
<i class="flag-ki"></i>
<i class="flag-km"></i>
<i class="flag-kn"></i>
<i class="flag-kp"></i>
<i class="flag-kr"></i>
<i class="flag-kw"></i>
<i class="flag-ky"></i>
<i class="flag-kz"></i>
<i class="flag-la"></i>
<i class="flag-lb"></i>
<i class="flag-lc"></i>
<i class="flag-li"></i>
<i class="flag-lk"></i>
<i class="flag-lr"></i>
<i class="flag-ls"></i>
<i class="flag-lt"></i>
<i class="flag-lu"></i>
<i class="flag-lv"></i>
<i class="flag-ly"></i>
<i class="flag-ma"></i>
<i class="flag-mc"></i>
<i class="flag-md"></i>
<i class="flag-me"></i>
<i class="flag-mf"></i>
<i class="flag-mg"></i>
<i class="flag-mh"></i>
<i class="flag-mk"></i>
<i class="flag-ml"></i>
<i class="flag-mm"></i>
<i class="flag-mn"></i>
<i class="flag-mo"></i>
<i class="flag-mp"></i>
<i class="flag-mq"></i>
<i class="flag-mr"></i>
<i class="flag-ms"></i>
<i class="flag-mt"></i>
<i class="flag-mu"></i>
<i class="flag-mv"></i>
<i class="flag-mw"></i>
<i class="flag-mx"></i>
<i class="flag-my"></i>
<i class="flag-mz"></i>
<i class="flag-na"></i>
<i class="flag-nc"></i>
<i class="flag-ne"></i>
<i class="flag-nf"></i>
<i class="flag-ng"></i>
<i class="flag-ni"></i>
<i class="flag-nl"></i>
<i class="flag-no"></i>
<i class="flag-np"></i>
<i class="flag-nr"></i>
<i class="flag-nu"></i>
<i class="flag-nz"></i>
<i class="flag-om"></i>
<i class="flag-pa"></i>
<i class="flag-pe"></i>
<i class="flag-pf"></i>
<i class="flag-pg"></i>
<i class="flag-ph"></i>
<i class="flag-pk"></i>
<i class="flag-pl"></i>
<i class="flag-pm"></i>
<i class="flag-pn"></i>
<i class="flag-pr"></i>
<i class="flag-ps"></i>
<i class="flag-pt"></i>
<i class="flag-pw"></i>
<i class="flag-py"></i>
<i class="flag-qa"></i>
<i class="flag-re"></i>
<i class="flag-ro"></i>
<i class="flag-rs"></i>
<i class="flag-ru"></i>
<i class="flag-rw"></i>
<i class="flag-sa"></i>
<i class="flag-sb"></i>
<i class="flag-sc"></i>
<i class="flag-sd"></i>
<i class="flag-se"></i>
<i class="flag-sg"></i>
<i class="flag-sh"></i>
<i class="flag-si"></i>
<i class="flag-sj"></i>
<i class="flag-sk"></i>
<i class="flag-sl"></i>
<i class="flag-sm"></i>
<i class="flag-sn"></i>
<i class="flag-so"></i>
<i class="flag-sr"></i>
<i class="flag-ss"></i>
<i class="flag-st"></i>
<i class="flag-sv"></i>
<i class="flag-sx"></i>
<i class="flag-sy"></i>
<i class="flag-sz"></i>
<i class="flag-tc"></i>
<i class="flag-td"></i>
<i class="flag-tf"></i>
<i class="flag-tg"></i>
<i class="flag-th"></i>
<i class="flag-tj"></i>
<i class="flag-tk"></i>
<i class="flag-tl"></i>
<i class="flag-tm"></i>
<i class="flag-tn"></i>
<i class="flag-to"></i>
<i class="flag-tr"></i>
<i class="flag-tt"></i>
<i class="flag-tv"></i>
<i class="flag-tw"></i>
<i class="flag-tz"></i>
<i class="flag-ua"></i>
<i class="flag-ug"></i>
<i class="flag-um"></i>
<i class="flag-us"></i>
<i class="flag-uy"></i>
<i class="flag-uz"></i>
<i class="flag-va"></i>
<i class="flag-vc"></i>
<i class="flag-ve"></i>
<i class="flag-vg"></i>
<i class="flag-vi"></i>
<i class="flag-vn"></i>
<i class="flag-vu"></i>
<i class="flag-wf"></i>
<i class="flag-ws"></i>
<i class="flag-ye"></i>
<i class="flag-yt"></i>
<i class="flag-za"></i>
<i class="flag-zm"></i>
<i class="flag-zw"></i>
<i class="flag-es-ct"></i>
<i class="flag-eu"></i>
<i class="flag-gb-eng"></i>
<i class="flag-gb-nir"></i>
<i class="flag-gb-sct"></i>
<i class="flag-gb-wls"></i>
<i class="flag-un"></i>
<p>Squared</p>
<i class="flag-us wn-flag-squared"></i>
```

View File

@@ -0,0 +1,70 @@
## Flash message
Displays a floating flash message on the screen.
### Display onload
```html
<p data-control="flash-message" data-interval="5" class="success">
This message is created from a static element. It will go away in 5 seconds.
</p>
```
<p data-control="flash-message" data-interval="5" class="info">
This message is created from a static element. It will go away in 5 seconds.
</p>
<br />
### Trigger
<p>
<a href="#" class="btn btn-primary" onclick="$.wn.flashMsg({text: 'The record has been successfully saved. This message will go away in 1 second.', 'class': 'success', 'interval': 1}); return false;">
Show Success
</a>
<a href="javascript:;" class="btn btn-danger" onclick="$.wn.flashMsg({text: 'Babam!', 'class': 'error'}); return false;">
Show Error
</a>
<a href="javascript:;" class="btn btn-warning" onclick="$.wn.flashMsg({text: 'Warning! Winter is too good for this world!', 'class': 'warning'}); return false;">
Show Warning
</a>
</p>
### Display static
A flash message can be rendered as a static element by attaching the `static` class. The `data-control` attribute is not needed.
<p class="flash-message static success">
Import completed successfully (success)
</p>
<p class="flash-message static info">
Informative info box is informational (info)
</p>
<p class="flash-message static warning">
Phasers have been set to stun (warning)
</p>
<p class="flash-message static error">
We couldn't help you with that (error)
</p>
### Data attributes
- data-control="flash-message" - enables the flash message plugin
- data-interval="2" - the interval to display the message in seconds, optional. Default: 2
### JavaScript API
```js
$.wn.flashMsg({
'text': 'Record saved.',
'class': 'success',
'interval': 3
})
```

View File

@@ -0,0 +1,180 @@
# Form
## Types
<form class="form-elements" role="form">
<div class="form-group span-left">
<label>First name</label>
<input type="text" name="" value="" class="form-control" />
</div>
<div class="form-group span-right">
<label>Last name</label>
<input type="text" name="" value="" class="form-control" />
</div>
<div class="form-group span-full">
<label>Address</label>
<input type="text" name="" value="" class="form-control" />
</div>
</form>
### Complete example
<!-- Form Elements -->
<form class="form-elements" role="form">
<!-- Text Input (Left) -->
<div class="form-group text-field span-left is-required">
<label>Input Left</label>
<input type="text" name="" value="" class="form-control" />
<p class="help-block">Example below help text here.</p>
</div>
<!-- Text Input (Right) -->
<div class="form-group text-field span-right is-required">
<label>Input Right</label>
<input type="text" name="" value="" class="form-control" />
<p class="help-block">Example below help text here.</p>
</div>
<!-- Text Input (Full) -->
<div class="form-group text-field span-full is-required">
<label>Input Full</label>
<p class="help-block before-field">Example above help text here.</p>
<input type="text" name="" value="" class="form-control" />
</div>
<!-- Drop down -->
<div class="form-group dropdown-field span-left">
<label>Drop Down</label>
<select class="form-control custom-select">
<option selected="selected" value="2">Approved</option>
<option value="3">Deleted</option>
<option value="1">New</option>
</select>
</div>
<!-- Grouped Drop down -->
<div class="form-group dropdown-field span-right">
<label>Grouped Drop Down</label>
<select class="form-control custom-select">
<optgroup label="NFC EAST">
<option>Dallas Cowboys</option>
<option>New York Giants</option>
<option>Philadelphia Eagles</option>
<option>Washington Redskins</option>
</optgroup><optgroup>
</optgroup><optgroup label="NFC NORTH">
<option>Chicago Bears</option>
<option>Detroit Lions</option>
<option>Green Bay Packers</option>
<option>Minnesota Vikings</option>
</optgroup>
<optgroup label="NFC SOUTH">
<option>Atlanta Falcons</option>
<option>Carolina Panthers</option>
<option>New Orleans Saints</option>
<option>Tampa Bay Buccaneers</option>
</optgroup>
<optgroup label="NFC WEST">
<option>Arizona Cardinals</option>
<option>St. Louis Rams</option>
<option>San Francisco 49ers</option>
<option>Seattle Seahawks</option>
</optgroup>
<optgroup label="AFC EAST">
<option>Buffalo Bills</option>
<option>Miami Dolphins</option>
<option>New England Patriots</option>
<option>New York Jets</option>
</optgroup>
<optgroup label="AFC NORTH">
<option>Baltimore Ravens</option>
<option>Cincinnati Bengals</option>
<option>Cleveland Browns</option>
<option>Pittsburgh Steelers</option>
</optgroup>
<optgroup label="AFC SOUTH">
<option>Houston Texans</option>
<option>Indianapolis Colts</option>
<option>Jacksonville Jaguars</option>
<option>Tennessee Titans</option>
</optgroup>
<optgroup label="AFC WEST">
<option>Denver Broncos</option>
<option>Kansas City Chiefs</option>
<option>Oakland Raiders</option>
<option>San Diego Chargers</option>
</optgroup>
</select>
</div>
<!-- Checkbox -->
<div class="form-group checkbox-field span-left is-required">
<div class="checkbox custom-checkbox">
<input name="checkbox" value="1" type="checkbox" id="checkbox_1">
<label for="checkbox_1">Enable Googie Berry Power-up</label>
<p class="help-block">Use this checkbox to enable the Googie Berry power-up specifically for this page. You can configure the Googie Berry power-up on the System Settings and Dashboard page.</p>
</div>
</div>
<!-- Switcher -->
<div class="form-group switch-field span-right">
<div class="field-switch">
<label>Would you like fries with that?</label>
<p class="help-block">Use this checkbox to enable the Googie Berry power-up specifically for this page. You can configure the Googie Berry power-up on the System Settings and Dashboard page.</p>
</div>
<label class="custom-switch">
<input type="checkbox" />
<span><span>On</span><span>Off</span></span>
<a class="slide-button"></a>
</label>
</div>
<!-- Radio List -->
<div class="form-group radio-field span-left is-required">
<label>Radio List</label>
<p class="help-block before-field">Where should you propose to your beautiful girl?</p>
<div class="radio custom-radio">
<input name="radio" value="1" type="radio" id="radio_1">
<label for="radio_1">Paris</label>
<p class="help-block">Do not send new comment notifications.</p>
</div>
<div class="radio custom-radio">
<input checked="checked" name="radio" value="2" type="radio" id="radio_2">
<label for="radio_2">Dubai</label>
<p class="help-block">Send new comment notifications only to post author.</p>
</div>
<div class="radio custom-radio">
<input name="radio" value="3" type="radio" id="radio_3">
<label for="radio_3">New Zealand</label>
<p class="help-block">Notify all users who have permissions to receive blog notifications.</p>
</div>
</div>
<!-- Checkbox List -->
<div class="form-group checkboxlist-field span-right is-required">
<label>Checkbox List</label>
<p class="help-block before-field">What cars would you like in your garage?</p>
<div class="checkbox custom-checkbox">
<input id="checkbox-example1" name="checkbox" value="1" type="checkbox">
<label class="choice" for="checkbox-example1"> Dodge Viper</label>
<p class="help-block">Do not send new comment notifications.</p>
</div>
<div class="checkbox custom-checkbox">
<input checked="checked" id="checkbox-example2" name="checkbox" value="2" type="checkbox">
<label class="choice" for="checkbox-example2"> GM Corvette</label>
<p class="help-block">Send new comment notifications only to post author.</p>
</div>
<div class="checkbox custom-checkbox">
<input id="checkbox-example3" name="checkbox" value="3" type="checkbox">
<label class="choice" for="checkbox-example3"> Porsche Boxter</label>
<p class="help-block">Notify all users who have permissions to receive blog notifications.</p>
</div>
</div>
</form>

View File

@@ -0,0 +1,235 @@
# Foundation
The foundation libraries are the core base of all scripts and controls. The goals of this library are:
- Well structured and readable code.
- Don't leave references to DOM elements.
- Unbind all event handlers.
- Write high-performance code (in cases when it's needed).
That's especially important on pages where users spend much time interacting with the page, like the CMS and Pages sections, but all back-end controls should follow these rules, because we never know when they are used.
## Why it's important to release the memory, DOM references and event handlers
A typical JavaScript control class instance consists of the following parts:
1. JavaScript object representing the control.
1. A reference to the corresponding DOM element. Usually it's the control's root element containing a tree with the control HTML markup.
1. A number of event handlers to handle user's interaction with the control.
If any of that components are not released we have these problems:
1. Non-released JavaScript objects increase the memory footprint. The more memory the application uses, the slower it works. Eventually it could result in a crashed tab or entire browser.
1. Non-released references to DOM elements could result in detached DOM trees. That, in turn, could result in thousands of invisible DOM elements living in a page, increasing the memory footprint and making the application less responsive.
1. Unbound event handlers usually result in non-released DOM elements, which is bad by itself, and also in the code which executes when the user interacts with the application and which should not be executed. That affects the performance.
## This is how to deal with those problems:
1. Remove the JavaScript object - usually by removing the data from the control's root element: `this.$el.removeData('oc.myControl')`
Clean all references to DOM elements. Usually it's done by assigning NULL to corresponding object properties.
1. Watch for any references caught by closures (or - better do not use closures, see below).
1. Unbind event handlers.
Winter Storm UI provides everything we need to meet the goals. Please read on to learn more!
## How to write quality code
OOP approach and prototypes should be used in all places. This approach automatically deals with closures that could retain references to scope variables. Typical class code template:
```js
function ($) { "use strict";
var SomeClass = function() {
this.init()
}
SomeClass.prototype.init = function (){
...
}
}
```
## Basics of writing disposable classes
If a class should be disposable (all UI controls should be disposable), the class should extend `$.wn.foundation.base` class. That class has two useful methods: `proxy(method)` and `dispose()`.
`proxy()` method is an alternative to jQuery's `$.proxy`, but as `$.wn.foundation.base` implements OOP approach, passing this parameter to the method is not required. This method is good for three reasons.
1. It's code is very simple and easily controllable and debuggable.
1. It caches bound functions and doesn't create new function as `$.proxy` does.
1. It automatically removes all cached bound functions when the object is disposed with dispose() method.
`dispose()` method in the base class cleans up bound methods cached by `proxy()` method and provides a common API for disposing objects. All classes that are supposed to do clean-up work, should override that method, do their own clean-up and call the base `dispose()` method.
Example of a disposable class:
```js
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var SomeDisposableClass = function(element) {
this.$el = $(element)
Base.call(this)
this.init()
}
SomeDisposableClass.prototype = Object.create(BaseProto)
SomeDisposableClass.prototype.constructor = SomeDisposableClass
SomeDisposableClass.prototype.init = function () {
}
SomeDisposableClass.prototype.dispose = function () {
this.$el = null
BaseProto.dispose.call(this)
}
}
```
A couple of important things to note:
1. The class constructor should call Base.call(this).
1. The class prototype should be replaced with a copy of the Base class prototype, and its constructor reference should be restored back to the class constructor. It should be done right after the class constructor and before any method is defined in the class prototype.
## Binding and unbinding events
When binding events, use this.proxy() to make references to event handlers. Always unbind events in dispose() method:
```js
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var SomeDisposableClass = function(element) {
this.$el = $(element)
Base.call(this)
this.init()
}
[...]
SomeDisposableClass.prototype.init = function () {
this.$el.on('click', this.proxy(this.onClick))
}
SomeDisposableClass.prototype.dispose = function () {
this.$el.off('click', this.proxy(this.onClick))
this.$el = null
BaseProto.dispose.call(this)
}
}
```
## Making disposable controls
UI controls should support two ways of disposing - with calling their `dispose()` method and with invoking the dispose-control handler. Also, disposable controls should mark their corresponding DOM elements as disposable, with Winter foundation API. Example:
```js
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var SomeDisposableControl = function(element) {
this.$el = $(element)
$.wn.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
...
SomeDisposableControl.prototype.init = function () {
this.$el.one('dispose-control', this.proxy(this.dispose))
}
SomeDisposableControl.prototype.dispose = function () {
this.$el.off('dispose-control', this.proxy(this.dispose))
this.$el = null
BaseProto.dispose.call(this)
}
}
```
`$.wn.foundation.controlUtils.markDisposable(element)` call in the constructor adds `data-disposable` attribute to the DOM element, allowing the framework to find all disposable elements in a container and dispose them by calling their dispose-control handler when it's required.
## Full example of a jQuery plugin that creates a disposable control
We already have a boilerplate code for jQuery code. Disposable controls approach just extends it. Don't forget to remove the data associated with controls from their DOM elements.
```js
+function ($) { "use strict";
var Base = $.wn.foundation.base,
BaseProto = Base.prototype
var SomeDisposableControl = function (element, options) {
this.$el = $(element)
this.options = options || {}
$.wn.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
SomeDisposableControl.prototype = Object.create(BaseProto)
SomeDisposableControl.prototype.constructor = SomeDisposableControl
SomeDisposableControl.prototype.init = function() {
this.$el.on('click', this.proxy(this.onClick))
this.$el.one('dispose-control', this.proxy(this.dispose))
}
SomeDisposableControl.prototype.dispose = function() {
this.$el.off('click', this.proxy(this.onClick))
this.$el.off('dispose-control', this.proxy(this.dispose))
this.$el.removeData('oc.someDisposableControl')
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)
}
SomeDisposableControl.DEFAULTS = {
someParam: null
}
// PLUGIN DEFINITION
// ============================
var old = $.fn.someDisposableControl
$.fn.someDisposableControl = function (option) {
var args = Array.prototype.slice.call(arguments, 1), items, result
items = this.each(function () {
var $this = $(this)
var data = $this.data('oc.someDisposableControl')
var options = $.extend({}, SomeDisposableControl.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.someDisposableControl', (data = new SomeDisposableControl(this, options)))
if (typeof option == 'string') result = data[option].apply(data, args)
if (typeof result != 'undefined') return false
})
return result ? result : items
}
$.fn.someDisposableControl.Constructor = SomeDisposableControl
$.fn.someDisposableControl.noConflict = function () {
$.fn.someDisposableControl = old
return this
}
// Add this only if required
$(document).render(function (){
$('[data-some-disposable-control]').someDisposableControl()
})
}(window.jQuery);
```

View File

@@ -0,0 +1,146 @@
# Icon library (Font Awesome 6)
Winter includes the [Font Awesome 6 Free](https://fontawesome.com/) icon set by default, allowing people to use over 1,500 free design icons and nearly 500 branding icons within the Backend or the CMS. We have also included compatibility with Font Awesome 4 classes for older plugins and themes.
For more information on the Font Awesome library, or to search through the available icons, visit the [Font Awesome website](https://fontawesome.com/search?m=free).
## Browsing the icons
You may browse the available icons on the Font Awesome website at the following URL:
https://fontawesome.com/search?m=free
Please note that we only include the free icons and brands.
## Using the icons
You may place icons just about anywhere by placing an inline tag (such as a `<i>` or `<span>` tag) and setting the class to the icon you wish to use:
```html
<i class="icon-camera-retro"></i> icon-camera-retro
<span class="icon-flag-checkered"></span> wn-icon-flag-checkered
```
<div class="frame" style="font-size: 22px;">
<i class="icon-camera-retro"></i> icon-camera-retro
<br />
<span class="icon-flag-checkered"></span> wn-icon-flag-checkered
</div>
Using the `wn-` prefix will allow you to prefix content inside the given tag with an icon:
```html
<i class="wn-icon-star">You're a star!</i>
<strong class="wn-icon-snowflake">Winter is coming.</strong>
```
<div class="frame" style="font-size: 22px;">
<i class="wn-icon-star">You're a star!</i>
<br>
<strong class="wn-icon-snowflake">Winter is coming.</strong>
</div>
You may also opt to use the standard Font Awesome classes as well:
```html
<i class="fas fa-star">You're a star!</i>
<strong class="fas fa-snowflake">Winter is coming.</strong>
```
<div class="frame" style="font-size: 22px;">
<i class="fas fa-star">You're a star!</i>
<br>
<strong class="fas fa-snowflake">Winter is coming.</strong>
</div>
### Icon styles
As with Font Awesome 6, Winter also includes three styles of icon: solid, regular and brand. By default, the icons in Winter use the solid style, which has the full set of free icons available.
To use a regular style icon, which is less pronounced but also has much less available icons, you may include the `icon-regular` class alongside your icon class. For brands, you may include the `icon-brand` class.
```html
<i class="icon-star"></i> Solid
<i class="icon-regular icon-star"></i> Regular
```
<div class="frame" style="font-size: 22px;">
<i class="icon-star"></i> Solid
<br>
<i class="icon-regular icon-star"></i> Regular
</div>
We also provide support for the Font Awesome style classes as well: `fas` for solid, `far` for regular and `fab` for brand.
### Icon sizes
Winter supports multiple sizing classes to control the size of the icons.
You may size by 1-10 times the regular size of icons.
```html
<i class="icon-camera-retro icon-10x"></i> icon-10x
<i class="icon-camera-retro icon-9x"></i> icon-9x
<i class="icon-camera-retro icon-8x"></i> icon-8x
<i class="icon-camera-retro icon-7x"></i> icon-7x
<i class="icon-camera-retro icon-6x"></i> icon-6x
<i class="icon-camera-retro icon-5x"></i> icon-5x
<i class="icon-camera-retro icon-4x"></i> icon-4x
<i class="icon-camera-retro icon-3x"></i> icon-3x
<i class="icon-camera-retro icon-2x"></i> icon-2x
<i class="icon-camera-retro icon-1x"></i> icon-1x
```
We also provide more classes that match common sizing prefixes in CSS frameworks such as `sm`, `lg`, etc.
```html
<i class="icon-camera-retro icon-2xs"></i> icon-2xs
<i class="icon-camera-retro icon-xs"></i> icon-xs
<i class="icon-camera-retro icon-sm"></i> icon-sm
<i class="icon-camera-retro icon-lg"></i> icon-lg
<i class="icon-camera-retro icon-xl"></i> icon-xl
<i class="icon-camera-retro icon-2xl"></i> icon-2xl
```
### Icon list items
You can apply icons to lists, allowing you to use the icon as the list item prefix as opposed to a standard circle.
You must include the `icon-ul` class to a `<ul>` tag, and then the `icon-li` class to all `<li>` items within to take advantage of this feature.
```html
<ul class="icon-ul">
<li class="icon-li icon-battery-0">Empty</li>
<li class="icon-li icon-battery">Low</li>
<li class="icon-li icon-battery-half">Charging</li>
<li class="icon-li icon-battery-full">Full</li>
</ul>
```
### Icon buttons
Feel free to use them alongside your buttons.
```html
<a class="btn btn-default wn-icon-refresh" href="#">
Refresh
</a>
<a class="btn btn-success wn-icon-shopping" href="#">
Checkout
</a>
<a class="btn btn-primary wn-icon-comment" href="#">
Comment
</a>
<a class="btn btn-danger wn-icon-trash" href="#">
Delete
</a>
<a class="btn btn-default wn-icon-cog" href="#">
Settings
</a>
<a class="btn btn-info wn-icon-info" href="#">
More Info
</a>
```

View File

@@ -0,0 +1,29 @@
# Input Hotkey API
Allows keyboard shortcuts (hotkeys) to be bound to an element's click event.
# Example
<button
class="btn btn-default"
data-hotkey="b"
onclick="alert('B is for Banana!')">
Press "B" on your keyboard
</button>
<button
class="btn btn-default"
data-hotkey="shift+r"
onclick="confirm('Shift gears...?')">
Press "Shift + R" on your keyboard
</button>
## Javascript API
If you use a selector other than a button or a link, you will need to add the `hotkeyVisible` property to the hotkey config.
$('html').hotKey({
hotkey: 'ctrl+s, cmd+s',
hotkeyVisible: false,
callback: doSomething
});

View File

@@ -0,0 +1,60 @@
# Input Monitoring
This will monitor the user input for unsaved changes and show a confirmation box if the user attempts to leave the page. The script adds the "oc-data-changed" class to the form element when the form data is changed.
```html
<form
data-change-monitor
data-window-close-confirm="There is unsaved data"
>
...
</form>
```
### Example
Click the "Mark changed" button and "Reload page".
<form
data-window-close-confirm="There is unsaved data"
data-change-monitor>
<button type="button" onclick="$(this).trigger('change')">
Mark changed
</button>
<button type="button" onclick="$(this).trigger('unchange.oc.changeMonitor')">
Mark saved
</button>
<hr />
<button type="button" onclick="window.location.reload()">
Reload page
</button>
</form>
## Supported data attributes
- data-change-monitor - enables the plugin form a form
- data-window-close-confirm - confirmation message to show when a browser window is closing and there is unsaved data
## Supported events
- change - marks the form data as "changed". The event can be triggered on any element within a form or on a form itself.
- unchange.oc.changeMonitor - marks the form data as "unchanged". The event can be triggered on any element within a form or on a form itself.
- pause.oc.changeMonitor - temporary pauses the change monitoring. The event can be triggered on any element within a form or on a form itself.
- resume.oc.changeMonitor - resumes the change monitoring. The event can be triggered on any element within a form or on a form itself.
## Triggered events
- changed.oc.changeMonitor - triggered when the form data changes.
- unchanged.oc.changeMonitor - triggered when the form data unchanges.
- ready.oc.changeMonitor triggered when the change monitor instance finishes initialization.
## JavaScript API
```js
$('#form').changeMonitor()
```

View File

@@ -0,0 +1,11 @@
# Input Preset API
Scripts that manage user input events.
# Example
<input type="text" id="presetExample1" placeholder="Type something" />
<input type="text"
data-input-preset="#presetExample1"
placeholder="Watch here"
disabled />

View File

@@ -0,0 +1,85 @@
# Input Trigger API
The API allows to change elements' visibility or status (enabled/disabled) basing on other elements' statuses. Example: enable a button if any checkbox inside another element is checked.
## Example
### Checked condition
<input type="checkbox" id="triggerChk1" />
<button class="btn disabled"
data-trigger-action="enable"
data-trigger="#triggerChk1"
data-trigger-condition="checked">
Check the checkbox
</button>
### Value condition
<p>
<input
type="text"
id="triggerTxt1"
value=""
onkeyup="$(this).trigger('change')"
placeholder="Enter 'foo' or 'bar' here"
class="form-control" />
</p>
<div
class="callout callout-success"
data-trigger-action="show"
data-trigger="#triggerTxt1"
data-trigger-condition="value[foo][bar]">
<div class="content">
Passphrase is valid!
</div>
</div>
## Supported data attributes
- data-trigger-action, values: show, hide, enable, disable, empty
- data-trigger: a CSS selector for elements that trigger the action (checkboxes)
- data-trigger-condition, values:
- checked: determines the condition the elements specified in the data-trigger should satisfy in order the condition to be considered as "true".
- unchecked: inverse condition of "checked".
- value[somevalue]: determines if the value of data-trigger equals the specified value (somevalue) the condition is considered "true".
- data-trigger-closest-parent: optional, specifies a CSS selector for a closest common parent for the source and destination input elements.
Example code:
```html
<input type="button" class="btn disabled"
data-trigger-action="enable"
data-trigger="#cblist input[type=checkbox]"
data-trigger-condition="checked" ... >
```
Multiple actions are supported:
```html
data-trigger-action="hide|empty"
```
Multie value conditions are supported:
```html
data-trigger-condition="value[foo][bar]"
```
### Supported events
- oc.triggerOn.update - triggers the update. Trigger this event on the element the plugin is bound to to force it to check the condition and update itself. This is useful when the page content is updated with AJAX.
- oc.triggerOn.afterUpdate - triggered after the element is updated
### JavaScript API
```html
$('#mybutton').triggerOn({
triggerCondition: 'checked',
trigger: '#cblist input[type=checkbox]',
triggerAction: 'enable'
})
```

View File

@@ -0,0 +1,724 @@
# Inspector control
Inspector is a visual configuration tool that is used in several places of Winter back-end. The most known usage of Inspector is the CMS components configuration feature, but Inspector is not limited with the CMS. In fact, it's a universal tool that can be used with any element on a back-end page.
The Inspector loads the configuration schema from an inspectable element, builds the user interface, and writes values entered by users back to the inspectable element. The first version of Inspector was supporting only a few scalar value types - strings and Booleans, without an option to edit any complex data.
The current version of Inspector allows to edit any imaginable data structures, including cases where users create enumerable data elements right in the Inspector interface.
This section describes the client-side Inspector API without going into details about the back-end usage of the data Inspector generates. Inspector accepts the configuration schema in JSON format and generates values in JSON format as well. Providing the configuration and interpreting the generated values is up to developers. For example, the CMS module uses information returned from component's defineProperties() method to generate the configuration JSON string and converts JSON values generated by Inspector to the components configuration in CMS templates. In this document we are focusing only on the JSON format.
## Configuring inspectable elements
Clicking an inspectable element displays Inspector for that element. Any HTML element could be made inspectable by adding data attributes to it. The required attributes are:
* `data-inspectable` - indicates that Inspector should be created when the element is clicked.
* `data-inspector-title` - sets the Inspector popup title.
* `data-inspector-config` - contains the Inspector configuration JSON string. If this attribute is not specified, the configuration is loaded from the server, see the [Dynamic configuration and dynamic items](#dynamic-configuration-and-dynamic-items) section below.
Inspectable elements should also contain a hidden input element used by Inspector for reading and writing values. The input element should be marked with the `data-inspector-values` data attribute.
Example inspectable element markup:
```html
<div
data-inspectable
data-inspector-title="Some inspectable element"
data-inspector-description="Some description">
<input
data-inspector-values
type="hidden"
value="JSON"/>
</div>
```
### Optional data attributes
There are several optional data attributes and features that could be defined in an inspectable element or in elements around it:
* `data-inspector-offset` - sets offset, in pixels, for the Inspector popup.
* `data-inspector-offset-x` - sets horizontal offset, in pixels, for the Inspector popup.
* `data-inspector-offset-y` - sets vertical offset, in pixels, for the Inspector popup.
* `data-inspector-placement` - sets defines placement for the Inspector popup, optional. If omitted, Inspector evaluates a placement automatically. Supported values: top, bottom, left, top.
* `data-inspector-fallback-placement` - sets less preferable placement for the Inspector popup, optional. This value is used if Inspector can't use the placement specified in data-inspector-placement. Supported values: top, bottom, left, top.
* `data-inspector-external-parameters` - if this attribute exists in any parent element of the inspectable element, the external parameters editors will be enabled in Inspector (unless property-specific rules cancel the external editor).
### Dynamic configuration and dynamic items
In case if the `data-inspector-config` attribute is missing in the inspectable element Inspector tries to load its configuration from the server. An important note - there should be a FORM element wrapping inspectable elements in order to use any dynamic features of Inspector.
The AJAX request used for loading the configuration from the server is named `onGetInspectorConfiguration`. The handler should be defined in the back-end controller and should return an array containing the Inspector configuration (in the PHP equivalent of the JSON configuration structure described later in this section), inspector title and description. Example of a server-side AJAX dynamic configuration request handler:
```php
public function onGetInspectorConfiguration()
{
// Load and use some values from the posted form
//
$someValue = Request::input('someValue');
... do some processing ...
return [
'configuration' => [
'properties' => [list of properties],
'title' => 'Inspector title',
'description' => 'Inspector description'
]
];
}
```
Some Inspector editors - (drop-down, set, autocomplete) support static and dynamic options. Dynamic options are requested from the server, rather than being defined in the configuration JSON string. For using this feature, the inspectable element must have the `data-inspector-class` attribute defined. The attribute value should contain a name of a PHP class corresponding to the inspectable element.
The server-side controller should use the `Backend\Traits\InspectableContainer` trait in order to provide the dynamic options loading. The inspectable PHP class (specified with `data-inspector-class`) must either have a method `get[Property]Options()`, where the [Property] part corresponds the name of the dynamic property, or `getPropertyOptions($propertyName)` method that is more universal and accepts the property name as a parameter. The methods should return the `options` array containing associative arrays with keys `option` and `value`. Example:
```php
public function getContextOptions()
{
$optionsArray = [];
$optionsArray[] = ['value' => 'create', 'title' => 'Create'];
$optionsArray[] = ['value' => 'update', 'title' => 'Update'];
$optionsArray[] = ['value' => 'delete', 'title' => 'Delete'];
return [
'options' => $optionsArray
];
}
```
### Container and popups
By default Inspector is displayed in a popup, but there's an option to display it right on the page, in a container element. To enable this option, all inspectable elements should be wrapped into another element with `data-inspector-container` attribute. The attribute value should be a CSS selector pointing to an element inside the wrapper. Example:
```html
<div data-inspector-container=".inspector-container">
<div class="inspector-container"></div>
<div data-inspectable ... ...>
<div data-inspectable ... ...>
</div>
```
The inner element will act as host element for Inspector when an inspectable element is clicked. The element should have the `inspector-container` class and can be optionally marked with `data-inspector-scrollable` attribute to make the Inspector scrollable. For the scrolling feature, the container element should have height defined explicitly.
When the container is used, Inspector is still displayed in a popup by default, but users can click an icon in the Inspector header to move it to the container.
## Data schema configuration
Inspector configuration, defined with `data-inspector-config` attribute or loaded from the server, should be an array containing a list of property definition. All examples in this section use JSON format. Below is an example of a configuration for two properties:
```json
[
{
"property": "firstName",
"title": "First name",
"type": "string"
},
{
"property": "lastName",
"title": "Last name",
"type": "string"
}
]
```
This configuration creates two text fields with titles "First name" and "Last name". When the data is saved back to the inspectable element (to the `data-inspector-values` hidden input element), it would have the following format:
```json
{"firstName":"John", "lastName":"Smith"}
```
Each property should have attributes `property`, `title` and `type`. The `type` attribute defines a type of an editor that should be created for the property. The supported editors are described further.
Other attributes supported by all (or most of the) property types are:
* `description` - description string, which is available in a tooltip displayed when a user overs the 'i' icon in the property editor.
* `group` - allows to group multiple properties. The attribute should contain a group name. Groups could be collapsed by users, making the Inspector interface less cluttered.
* `showExternalParam` - enables the inspector parameter editor for the property. External parameters are currently used only by the CMS. Note that some property types do not support external property editors. See also `data-inspector-external-parameters` attribute described above.
* `placeholder` - text to display in the editor if property value is empty.
* `validation` - validation configuration. See the complete validation description below.
* `default` - default property value. The property value format depends on the property type - for the `string` type it's an array, for `stringList` type it's an array of strings. See more details below.
All other configuration properties are specific for different property types.
### String editor
String editor allows entering a single line of a text and represented with a simple input text field. The editor doesn't have any specific parameters. The optional `default` parameter for the editor should contain a string.
```json
{
"property": "firstName",
"title": "First name",
"type": "string",
"default": "John"
}
```
The editor generates string values:
```json
{"firstName":"Sam"}
```
### Text editor
Text editor allows entering multi-line long text values in a popup window. The editor doesn't have any specific parameters. The optional `default` parameter for the editor should contain a string.
```json
{
"property": "description",
"title": "Description",
"type": "text",
"default": "This is a default description"
}
```
The editor generates string values:
```json
{"description":"This is a description"}
```
### String list editor
Allows users to enter lists of strings. The editor opens in a popup window and displays a text area. Each line of text represents an element in the result array. The optional `default` parameter should contain an array of strings. Example:
```json
{
"property": "items",
"title": "Items"
"type": "stringList",
"default": ["String 1", "String 2"]
}
```
A value generated by the editor is an array of strings, for example:
```json
{"items":["String 1","String 2","String 3"]}
```
### Autocomplete editor
This editor works like the `string` editor, but includes the autocomplete feature. Autocompletion options can be specified statically, with the `items` parameter or loaded dynamically. Example with static options:
```json
{
"property": "condition",
"title": "Condition"
"type": "autocomplete",
"items": {"start": "Start", "end": "End"}
}
```
The items are specified as a key-value object. The `items` parameter is optional, if it's not provided, the items will be loaded from the server - see [Dynamic configuration and dynamic items](#dynamic-configuration-and-dynamic-items) section above.
Values generated by the editor are strings. Example:
```json
{"condition":"start"}
```
Fields of this type do not support external property editors.
### Checkbox editor
Properties of this type are represented with a checkbox in the Inspector UI. This property doesn't have any special parameters. The `default` parameter, if specified, should contain a Boolean value or string values "true", "false", "1", "0". Example:
```json
{
"property": "enabled",
"title": "Enabled",
"type": "checkbox",
"default": true
}
```
Values generated by the editor are 0 (unchecked) or 1 (checked). Example:
```json
{"enabled":1}
```
### Dropdown editor
Displays a drop-down list. Options for the drop-down list can be specified statically with the `options` attribute or loaded from the server dynamically. Example:
```json
{
"property": "action",
"title": "Action",
"type": "dropdown",
"options": {
"show": "Show",
"hide": "Hide",
"enable": "Enable",
"disable": "Disable",
"empty": "Empty"
}
}
```
The `options` attribute should be a key-value object. If the attribute is not specified, Inspector will try to load options from the server - see [Dynamic configuration and dynamic items](#dynamic-configuration-and-dynamic-items) section above.
The editor generates a string value corresponding to the selected option, for example:
```json
{"action":"hide"}
```
### Dictionary editor
Dictionary editor allows to create key-value pairs with a simple user interface consisting of a table with two columns. The `default` parameter, if specified, should contain a key-value object. Example:
```json
{
"property": "options",
"title": "Options",
"type": "dictionary",
"default": {"option1": "Option 1"}
}
```
The editor generates an object value, for example:
```json
{"options":{"option1":"Option 1","option2":"Option 2"}}
```
The dictionary editor supports validation for the entire set (`required` and `length` validators) and for keys and values separately. See the [validation description](#defining-the-validation-rules) further in this document. The `validationKey` and `validationValue` define validation for keys and values, for example:
```json
{
"property": "options",
"title": "Options",
"type": "dictionary",
"validation": {
"required": {
"message": "Please create options"
},
"length": {
"min": {
"value": 2,
"message": "Create at least two options."
}
}
},
"validationKey": {
"regex": {
"pattern": "^[a-z]+$",
"message": "Keys can contain only lowercase Latin letters"
}
},
"validationValue": {
"regex": {
"pattern": "^[a-zA-Z0-9]+$",
"message": "Values can contain only Latin letters and digits"
}
}
}
```
### Object editor
Allows to define an object with specific properties editable by users. Object properties are specified with the `properties` attribute. The value of the attribute is an array, which has exactly the same structure as the Inspector properties array.
```json
{
"property": "address",
"title": "Address",
"type": "object",
"properties": [
{
"property": "streetAddress",
"title": "Street address",
"type": "string"
},
{
"property": "city",
"title": "City",
"type": "string"
},
{
"property": "country",
"title": "Country",
"type": "dropdown",
"options": {"us": "US", "ca": "Canada"}
}
]
}
```
The example above creates an object with three properties. Two of them are displayed as text fields, and the third as a drop-down.
Object editor values are objects. Example:
```json
{
"address": {
"streetAddress":"321-210 Second ave",
"city":"Springfield",
"country":"us"
}
}
```
The object properties can be of any type supported by Inspector, including other objects.
There's a way to exclude an object from Inspector values completely, if one of the object fields is empty. The field is identified with `ignoreIfPropertyEmpty` parameter. For example:
```json
{
"property": "address",
"title": "Address",
"type": "object",
"ignoreIfPropertyEmpty": "title",
"properties": [
{
"property": "streetAddress",
"title": "Street address",
"type": "string"
},
{
"property": "city",
"title": "City",
"type": "string"
}
]
}
```
In the example above, if the street address is not specified, the object ("address") will be completely removed from the Inspector output. If there are any validation rules defined on other object properties and the required property is empty, those rules will be ignored.
A `default` value for the editor, if specified, should be an object with the same properties as defined in the `properties` configuration parameter.
Object editors do not support the external property editor feature.
### Object list editor
The object list editor allows users to create multiple objects with a pre-defined structure. For example, it could be used for creating a list of person, where each person has a name and address.
The properties of objects that can be created with the editor are defined with `itemProperties` parameter. The parameter should contain an array of properties, similar to Inspector configuration array. Another required parameter is `titleProperty`, which identifies a property that should be used as a title in Inspector UI. Example configuration:
```json
{
"property": "people",
"title": "People",
"type": "objectList",
"titleProperty": "fullName",
"itemProperties": [
{
"property": "fullName",
"title": "Full name",
"type": "string"
},
{
"property": "address",
"title": "Address",
"type": "string"
}
]
}
```
The array of properties defined with `itemProperties` supports all property types.
The Object List editor type doesn't support default values.
By default the value created by the editor of this type is a non-associative array:
```json
{
"people":[
{"fullName":"John Smith","address":"Palo Alto"},
{"fullName":"Bart Simpson","address":"Springfield"}
]
}
```
If the result value should be an associative array (object), use the `keyProperty` configuration option. The option value should refer to a property that should be used as a key. The key property can use only the string or drop-down editors, its value should be unique and cannot be empty. Example:
```json
{
"property": "people",
"title": "People",
"type": "objectList",
"titleProperty": "fullName",
"keyProperty": "login",
"itemProperties": [
{
"property": "fullName",
"title": "Full name",
"type": "string"
},
{
"property": "login",
"title": "Login",
"type": "string"
},
{
"property": "address",
"title": "Address",
"type": "string"
}
]
}
```
The `login` property in the example above will be used as a key in the result value:
```json
{
"people":{
"john":{"fullName":"John Smith","address":"Palo Alto"},
"bart":{"fullName":"Bart Simpson","address":"Springfield"}
}
}
```
### Set editor
The set editor allows users to select multiple predefined options with checkboxes. Set items can be specified statically with the configuration, using the `items` parameter, or loaded dynamically. Example with static items definition:
```json
{
"property": "context",
"title": "Context",
"type": "set",
"items": {
"create": "Create",
"update": "Update",
"preview": "Preview"
},
"default": ["create", "update"]
}
```
The `items` attribute should be a key-value object. If the attribute is not specified, Inspector will try to load options from the server - see [Dynamic configuration and dynamic items](#dynamic-configuration-and-dynamic-items) section above.
The `default` parameter, if specified, should be an array listing item keys selected by default.
Set editors do not support the external property editor feature.
## Defining the validation rules
Inspector support several validation rules that can be applied to properties. Validation rules can be applied to top-level properties as well as to internal property definitions of object and object list editors. There are two ways to define validation rules - the legacy syntax and the new syntax.
The legacy syntax is supported for the backwards compatibility with existing CMS components definitions. This syntax will always be supported, but it's limited, and cannot be mixed with the new syntax. Example of the legacy syntax:
```json
{
"property": "name",
"title": "Name",
"type": "string",
"required": true,
"validationPattern": "^[a-zA-Z]+$"
"validationMessage": "The Name field is required and can contain only Latin letters.",
}
```
The legacy syntax supports only two validation rules - required and regular expression. The new syntax is much more flexible and extendable:
```json
{
"property": "name",
"title": "Name",
"type": "string",
"validation": {
"required": {
"message": "The Name field is required"
},
"regex": {
"message": "The Name field can contain only Latin letters.",
"pattern": "^[a-zA-Z]+$"
}
}
}
```
The key value in the `validation` object refers to a validator (see below). Validators are configured with objects, which properties depend on a validator. One property - `message` is common for all validators.
### required validator
Checks if a value is not empty. The validator can be used with any editor, including complex editors (sets, dictionaries, object lists, etc.). Example:
```json
{
"property": "name",
"title": "Name",
"type": "string",
"validation": {
"required": {
"message": "The Name field is required"
}
}
}
```
### regex validator
Validates string values with a regular expression. The validator can be use only with string-typed editors. Example:
```json
{
"property": "name",
"title": "Name",
"type": "string",
"validation": {
"regex": {
"message": "The Name field can contain only Latin letters",
"pattern": "^[a-z]+$",
"modifiers": "i"
}
}
}
```
The regular expression is specified with the required `pattern` parameter. The `modifiers` parameter is optional and can be used for setting regular expression modifiers.
### integer validator
Checks if the value is integer and can optionally validate if the value is within a specific interval. The validator can be used only with string-typed editors. Example:
```json
{
"property": "numOfColumns",
"title": "Number of Columns",
"type": "string",
"validation": {
"integer": {
"message": "The Number of Columns field should contain an integer value",
"allowNegative": true,
"min": {
"value": -10,
"message": "The number of columns should not be less than -10."
},
"max": {
"value": 10,
"message": "The number of columns should not be greater than 10."
}
}
}
}
```
Supported parameters:
* `allowNegative` - optional, determines if negative values are allowed. By default negative values are not allowed.
* `min` - optional object, defines the minimum allowed value and error message. Object fields:
* `value` - defines the minimum value.
* `message` - optional, defines the error message.
* `max` - optional object, defines the maximum allowed value and error message. Object fields:
* `value` - defines the maximum value.
* `message` - optional, defines the error message.
### float validator
Checks if the value is a floating point number. The parameters for this validator match the parameters of the **integer** validator described above. Example:
```json
{
"property": "amount",
"title": "Amount",
"type": "string",
"validation": {
"float": {
"message": "The Amount field should contain a positive floating point value."
}
}
}
```
Valid floating point number formats:
* 10
* 10.302
* -10 (if `allowNegative` is `true`)
* -10.84 (if `allowNegative` is `true`)
### length validator
Checks if a string, array or object is not shorter or longer than specified values. This validator can work with the string, text, set, string list, dictionary and object list editors. In multiple-value editors (set, string list, dictionary and object list) it validates the number of items created in the editor.
> **Note**: the `length` validator doesn't validate empty values. For example, if it's applied to a set editor, and the set is empty, the validation will pass regardless of the `min` and `max` parameter values. Use the `required` validator together with the `length` validator to make sure that the value is not empty before the length validation is applied.
```json
{
"property": "name",
"title": "Name",
"type": "string",
"validation": {
"length": {
"min": {
"value": 2,
"message": "The name should not be shorter than two letters."
},
"max": {
"value": 10,
"message": "name should not be longer than 10 letters."
}
}
}
}
```
Supported parameters:
* `min` - optional object, defines the minimum allowed length and error message. Object fields:
* `value` - defines the minimum value.
* `message` - optional, defines the error message.
* `max` - optional object, defines the maximum allowed length and error message. Object fields:
* `value` - defines the maximum value.
* `message` - optional, defines the error message.
## Inspector events
Inspector triggers several events on the inspectable elements.
### change
The `change` event is triggered after Inspector applies updated values to the inspectable element. The event is triggered only if the user has changed values in the Inspector UI.
### showing.oc.inspector
The `showing.oc.inspector` event is triggered before Inspector is displayed. The event handler can optionally stop the process with calling `ev.isDefaultPrevented()`. Example - prevent Inspector showing:
```js
$(document).on('showing.oc.inspector', 'div[data-inspectable]', function(ev, data){
ev.preventDefault()
})
```
The handler could perform any required processing, even asynchronous, and then call the callback function passed to the handler, to continue showing the Inspector. In this case the handler should call `ev.stopPropagation()` method to stop the default Inspector initialization. Example - continue showing after some processing:
```js
$(document).on('showing.oc.inspector', 'div[data-inspectable]', function(ev, data){
ev.stopPropagation()
// The callback function can be called asynchronously
data.callback()
})
```
### hiding.oc.inspector
The `hiding.oc.inspector` is called before Inspector hiding process starts. The handler can stop the hiding with calling `ev.preventDefault()`. Example:
```js
$(document).on('hiding.oc.inspector', 'div[data-inspectable]', function(ev, data){
if (!confirm('Allow hiding?')) {
ev.preventDefault()
}
})
```
The values entered in Inspector are available through the `values` element of the second handler argument:
```js
$(document).on('hiding.oc.inspector', 'div[data-inspectable]', function(ev, data){
console.log(data.values)
})
```
### hidden.oc.inspector
The `hidden.oc.inspector` is triggered after Inspector is hidden.

View File

@@ -0,0 +1,336 @@
### Basic example
<div class="control-list">
<table class="table data">
<thead>
<tr>
<th class="sort-desc"><a href="/">Title</a></th>
<th class="active sort-asc"><a href="/">Created</a></th>
<th><span>Categories</span></th>
<th><span>Updated</span></th>
<th class="list-setup"><a href="/" title="List options"></a></th>
</tr>
</thead>
<tbody>
<tr>
<td>Welcome to Winter</td>
<td>Oct 01, 2013</td>
<td>News</td>
<td>Oct 01, 2013</td>
<td>&nbsp;</td>
</tr>
</tbody>
</table>
</div>
### Complete example
<div class="control-list">
<table class="table data" data-control="rowlink">
<thead>
<tr>
<th class="list-checkbox">
<div class="checkbox custom-checkbox nolabel">
<input type="checkbox" id="checkboxAll" />
<label for="checkboxAll"></label>
</div>
</th>
<th class="sort-desc"><a href="/">Title</a></th>
<th class="active sort-asc"><a href="/">Created</a></th>
<th class="sort-desc"><a href="/">Author</a></th>
<th><span>Categories</span></th>
<th><span>Published</span></th>
<th><span>Updated</span></th>
<th class="list-setup"><a href="/" title="List options"></a></th>
</tr>
</thead>
<tbody>
<tr>
<td class="list-checkbox nolink">
<div class="checkbox custom-checkbox nolabel">
<input id="checkbox_1" type="checkbox" />
<label for="checkbox_1">Check</label>
</div>
</td>
<td><a href="/">Welcome to Winter</a></td>
<td>Oct 01, 2013</td>
<td>Adam Person</td>
<td>News</td>
<td>Oct 01, 2013</td>
<td>Oct 01, 2013</td>
<td>&nbsp;</td>
</tr>
<tr class="active">
<td class="list-checkbox nolink">
<div class="checkbox custom-checkbox nolabel">
<input id="checkbox_2" type="checkbox" checked="checked" /><label for="checkbox_2">Check The marketplace is open!</label>
</div>
</td>
<td><a href="/">The marketplace is open!</a></td>
<td>Oct 15, 2013</td>
<td>Sam Georges</td>
<td>Features</td>
<td>Oct 16, 2013</td>
<td>Oct 16, 2013</td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="list-checkbox nolink">
<div class="checkbox custom-checkbox nolabel">
<input id="checkbox_3" type="checkbox" />
<label for="checkbox_3">Check Welcome to the Builder!</label>
</div>
</td>
<td><a href="/">Welcome to the Builder!</a></td>
<td>Oct 21, 2013</td>
<td>Alexey Bobkov</td>
<td>News, Features</td>
<td>Oct 21, 2013</td>
<td>Oct 21, 2013</td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="list-checkbox nolink">
<div class="checkbox custom-checkbox nolabel">
<input id="checkbox_4" type="checkbox" />
<label for="checkbox_4">Check Components explained</label>
</div>
</td>
<td><a href="/">Components explained</a></td>
<td>Nov 12, 2013</td>
<td>Alexey Bobkov</td>
<td>Tutorials</td>
<td>Nov 12, 2013</td>
<td>Nov 12, 2013</td>
<td>&nbsp;</td>
</tr>
<tr>
<td class="list-checkbox nolink">
<div class="checkbox custom-checkbox nolabel">
<input id="checkbox_5" type="checkbox" />
<label for="checkbox_5">Check Creating a module in 90 seconds</label>
</div>
</td>
<td><a href="/">Creating a module in 90 seconds</a></td>
<td>Nov 15, 2013</td>
<td>Sam Georges</td>
<td>Tutorials</td>
<td>Nov 15, 2013</td>
<td>Nov 15, 2013</td>
<td>&nbsp;</td>
</tr>
</body>
</table>
<div class="list-footer">
<div class="list-pagination">
<div class="control-pagination">
<span class="page-iteration">1-5 of 20</span>
<a href="#" class="page-back" title="Previous page"></a><a href="#" class="page-next" title="Next page"></a>
</div>
</div>
</div>
</div>
### Empty list
Use the `no-data` class to display a list that contains no records.
<div class="control-list">
<table class="table data">
<thead>
<tr>
<th class="sort-desc"><a href="/">Title</a></th>
<th class="active sort-asc"><a href="/">Created</a></th>
</tr>
</thead>
<tbody>
<tr class="no-data">
<td colspan="100" class="nolink">
<p class="no-data">
There are no records in this view.
</p>
</td>
</tr>
</tbody>
</table>
</div>
### Row classes
The following colored classes are available to use on the table row elements.
<div class="control-list">
<table class="table data">
<thead>
<tr>
<th class="sort-desc"><a href="/">Class</a></th>
</tr>
</thead>
<tbody>
<tr><td>Normal text</td></tr>
<tr class="hidden"><td>.hidden</td></tr>
<tr class="strike"><td>.strike</td></tr>
<tr class="frozen"><td>.frozen</td></tr>
<tr class="processing"><td>.processing</td></tr>
<tr class="negative"><td>.negative</td></tr>
<tr class="positive"><td>.positive</td></tr>
<tr class="disabled"><td>.disabled / .deleted</td></tr>
<tr class="new"><td>.new / .important</td></tr>
<tr class="safe"><td>.safe / .special</td></tr>
</tbody>
</table>
</div>
### Status column
It might be fun to include a status column!
<div class="control-list">
<table class="table data">
<thead>
<tr>
<th style="width: 150px"><span>Status</span></th>
<th class="active sort-asc"><a href="/">Title</a></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class="wn-icon-circle text-muted">
Draft
</span>
</td>
<td>Welcome to Winter</td>
</tr>
<tr>
<td>
<span class="wn-icon-circle text-info">
Pending
</span>
</td>
<td>What a wonderful day</td>
</tr>
<tr>
<td>
<span class="wn-icon-circle text-success">
Approved
</span>
</td>
<td>The sun is shining</td>
</tr>
<tr>
<td>
<span class="wn-icon-circle text-danger">
Cancelled
</span>
</td>
<td>The weather is sweet here</td>
</tr>
</tbody>
</table>
</div>
### Badge column
You can also include an icon badge inside a column.
<div class="control-list">
<table class="table data">
<thead>
<tr>
<th style="width: 150px"><span>Status</span></th>
<th class="active sort-asc"><a href="/">Title</a></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class="list-badge badge-info">
<i class="icon-info"></i>
</span>
Draft
</td>
<td>Welcome to Winter</td>
</tr>
<tr>
<td>
<span class="list-badge badge-warning">
<i class="icon-exclamation"></i>
</span>
Pending
</td>
<td>What a wonderful day</td>
</tr>
<tr>
<td>
<span class="list-badge badge-success">
<i class="icon-check"></i>
</span>
Approved
</td>
<td>The sun is shining</td>
</tr>
<tr>
<td>
<span class="list-badge badge-danger">
<i class="icon-times"></i>
</span>
Cancelled
</td>
<td>The weather is sweet here</td>
</tr>
</tbody>
</table>
</div>
### Linking rows
You may link an entire row by adding the `data-control="rowlink"` attribute to the table element. The first table data (TD) column with an anchor will be used to link the entire row. To bypass this behavior, simply add the `nolink` class to the column.
<div class="control-list">
<table class="table data" data-control="rowlink">
<tbody>
<tr>
<td>
<a href="https://wintercms.com">Link to this</a>
</td>
<td>Row will be linked</td>
<td>This will also be linked</td>
<td class="nolink">No link applied here</td>
</tr>
</tbody>
</table>
</div>
### Button column
You may add a small button to a list column by adding the `column-button` class to the table data (TD) element.
<div class="control-list">
<table class="table data" data-control="rowlink">
<thead>
<tr>
<th style="width: 150px"><span>Action</span></th>
<th><a href="javascript:;">Name</a></th>
</tr>
</thead>
<tbody>
<tr>
<td class="column-button nolink">
<a
href="http://google.com"
target="_blank"
class="btn btn-secondary btn-sm">
Open Google
</a>
</td>
<td>
<a href="javascript:;">
Petoria
</a>
</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,91 @@
# Loading indicators
## Container Loading Indicator
#### Loading Indicator
A loading indicator used in a container.
<div class="loading-indicator-container">
<div class="loading-indicator">
<span></span>
</div>
<p>This is some content inside the container</p>
<p>The loading indicator must be prepended to it</p>
</div>
#### Text Loading Indicator
A loading indicator can have text by adding a `<div>` element inside.
<div class="loading-indicator-container">
<div class="loading-indicator">
<span></span>
<div>Loading...</div>
</div>
</div>
#### Loading Indicator Sizes
A loading indicator can have a size by adding `size-X` to the container. These sizes are available: **size-small**.
<div class="loading-indicator-container">
<div class="loading-indicator size-small">
<span></span>
<div>Loading (size-small)</div>
</div>
</div>
#### Loading Indicator Alignment
A loading indicator can be aligned to the center by adding `indicator-center` to the container and/or indicator.
<div class="loading-indicator-container">
<div class="loading-indicator indicator-center">
<span></span>
</div>
</div>
You may add some optional text:
<div class="loading-indicator-container">
<div class="loading-indicator indicator-center">
<span></span>
<div>Loading...</div>
</div>
</div>
# Example
<div class="loading-indicator-container">
<div class="loading-indicator">
<span></span>
</div>
<p>This is some content inside the container</p>
<p>The loading indicator must be prepended to it</p>
</div>
<div class="loading-indicator-container">
<div class="loading-indicator">
<span></span>
<div>Loading...</div>
</div>
</div>
<div class="loading-indicator-container">
<div class="loading-indicator indicator-inset">
<span></span>
<div>Loading (inset)</div>
</div>
</div>
<div class="loading-indicator-container">
<div class="loading-indicator size-small">
<span></span>
<div>Loading (size-small)</div>
</div>
</div>
<div class="loading-indicator-container">
<div class="loading-indicator indicator-center">
<span></span>
</div>
</div>

View File

@@ -0,0 +1,23 @@
### Basic example
<div class="control-pagination">
<span class="page-iteration">Displayed records: 1-5 of 20</span>
<a href="#" class="page-back" title="Previous page"></a><a href="#" class="page-next" title="Next page"></a>
</div>
### Complete example
<div class="control-pagination">
<span class="page-iteration">Displayed records: 1-5 of 20</span>
<span class="page-first" title="First page"></span>
<span class="page-back" title="Previous page"></span>
<select
name="page"
class="form-control custom-select select-no-search">
<option value="1" selected>1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<a href="#" class="page-next" title="Next page"></a>
<a href="#" class="page-last" title="Last page"></a>
</div>

View File

@@ -0,0 +1,131 @@
# Popover
Renders a richer version of a tooltip, called a popover.
## Examples
### Basic usage
You may add `data-control="popover"` to an anchor or button to activate a popover. Use the `data-content` attribute to specify the contents.
<a
href="javascript:;"
class="btn btn-primary"
data-control="popover"
data-content="I am a standard popover">
Basic popover
</a>
### Template content
Define the popover content as a template and reference it with `data-content-from="#myPopoverContent"`.
```html
<script type="text/template" id="myPopoverContent">
<div class="popover-head">
<h3>Popover</h3>
<button type="button" class="close" data-dismiss="popover">&times;</button>
</div>
<div class="popover-body">
I am a popover
</div>
</script>
```
<div style="display:none" id="myPopoverContent">
<div class="popover-head">
<h3>Popover</h3>
<button type="button" class="close" data-dismiss="popover">&times;</button>
</div>
<div class="popover-body">
I am a popover
</div>
</div>
<a
href="javascript:;"
class="btn btn-primary"
data-control="popover"
data-width="200"
data-content-from="#myPopoverContent">
Template popover
</a>
### Event specified content
```js
$('#btn1').on('showing.oc.popover', function(e, popover) {
popover.options.content = '<div class="popover-body">Some other content</div>'
})
```
<a
href="javascript:;"
class="btn btn-primary"
data-control="popover"
data-placement="right"
id="btn1">
Event content popover
</a>
<script>
$(document).ready(function() {
$('#btn1').on('showing.oc.popover', function(e, popover) {
popover.options.content = '<div class="popover-body">Some other content</div>'
})
})
</script>
## JavaScript API
```js
$('#element').ocPopover({
content: '<p>This is a popover</p>'
placement: 'top'
})
```
### Supported methods
`.ocPopover('hide')`
Closes the popover. There are 3 ways to close the popover: call it's `hide()` method, trigger the `close.oc.popover` on any element inside the popover or click an element with attribute `data-dismiss="popover"` inside the popover.
### Supported options
- `placement`: top | bottom | left | right | center. The placement could automatically be changed if the popover doesn't fit into the desired position.
- `fallbackPlacement`: top | bottom | left | right. The placement to use if the default placement and all other possible placements do not work. The default value is "bottom".
- `content`: content HTML string or callback
- `contentFrom`: selector to source the content HTML
- `width`: content width, optional. If not specified, the content width will be used.
- `modal`: make the popover modal
- `highlightModalTarget`: "pop" the popover target above the overlay, making it highlighted. The feature assigns the target position relative.
- `closeOnPageClick`: close the popover if the page was clicked outside the popover area.
- `container`: the popover container selector or element. The default container is the document body. The container must be relative positioned.
- `containerClass` - a CSS class to apply to the popover container element
- `offset` - offset in pixels to add to the calculated position, to make the position more "random"
- `offsetX` - X offset in pixels to add to the calculated position, to make the position more "random". If specified, overrides the offset property for the bottom and top popover placement.
- `offsetY` - Y offset in pixels to add to the calculated position, to make the position more "random". If specified, overrides the offset property for the left and right popover placement.
- `useAnimation`: adds animation to the open and close sequence, the equivalent of adding the CSS class 'fade' to the containerClass.
### Supported events
- `showing.oc.popover` - triggered before the popover is displayed. Allows to override the popover options (for example the content) or cancel the action with e.preventDefault()
- `show.oc.popover` - triggered after the popover is displayed.
- `hiding.oc.popover` - triggered before the popover is closed. Allows to cancel the action with e.preventDefault()
- `hide.oc.popover` - triggered after the popover is hidden.

View File

@@ -0,0 +1,152 @@
# Popups
Displays a modal popup, based on the Bootstrap modal implementation.
- [Examples](#examples)
- [Inline popups](#inline-popups)
- [Remote popups](#remote-popups)
- [API documentation](#api-docs)
<a name="examples"></a>
## Examples
<a data-toggle="modal" href="#contentBasic" class="btn btn-primary btn-lg">Launch basic content</a>
<div class="control-popup modal fade" id="contentBasic" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<p>This is a very basic example of a popup...</p>
</div>
</div>
</div>
</div>
<a data-toggle="modal" href="#content-confirmation" class="btn btn-primary btn-lg">Launch Confirmation dialog</a>
<div class="control-popup modal fade" id="content-confirmation" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Are you sure you wanna do that?</h4>
</div>
<div class="modal-body">
<p>This is your last chance. After this, there is no turning back.</p>
<p>You take the blue pill - the story ends, you wake up in your bed and believe whatever you want to believe. You take the red pill - you stay in Wonderland, and I show you how deep the rabbit hole goes.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Blue Pill</button>
<button type="button" class="btn btn-primary" data-dismiss="modal">Red Pill</button>
</div>
</div>
</div>
</div>
<a name="inline-popups"></a>
## Inline popups
An inline popup places the popup content inside the current page, hidden from the view. For example, this container will not be visible on the page.
```html
<div class="control-popup modal fade" id="contentBasic">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<p>This is a very basic example of a popup...</p>
</div>
</div>
</div>
</div>
```
Use the `data-toggle="modal"` HTML attribute to launch this container as a popup.
```html
<a data-toggle="modal" href="#contentBasic" class="btn btn-primary btn-lg">
Launch basic content
</a>
```
<a name="remote-popups"></a>
## Remote popups
Content for the popup can be loaded remotely using an AJAX request. Use the `data-handler` attribute to populate a popup with the contents of an AJAX handler.
```html
<a
data-control="popup"
data-handler="onLoadContent"
href="javascript:;"
class="btn btn-primary btn-lg">
Launch Ajax Form
</a>
```
Using the `data-ajax` attribute you can refer to an external file or URL directly.
```html
<a
data-control="popup"
data-ajax="popup-content.htm"
href="javascript:;"
class="btn btn-primary btn-lg">
Launch Ajax Form
</a>
```
The partial for your rendered popup should follow this structure:
```html
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title">
<!-- Modal header title goes here -->
Send email
</h4>
</div>
<div class="modal-body">
<!-- Any popup content goes here -->
<?= $this->customFormWidget->render() ?>
</div>
<div class="modal-footer">
<!-- Popup action buttons go here -->
<button
type="submit"
class="btn btn-primary wn-icon-send"
data-load-indicator="Sending">
Send
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.relation.close')) ?>
</button>
</div>
```
<a name="api-docs"></a>
## API documentation
### Options:
- `content` - content HTML string or callback
### Data attributes
- `data-control="popup"` - enables the ajax popup plugin
- `data-ajax="popup-content.htm"` - ajax content to load
- `data-handler="onLoadContent"` - Winter ajax request name
- `data-keyboard="false"` - Allow popup to be closed with the keyboard
- `data-extra-data="file_id: 1"` - Winter ajax request data
- `data-size="large"` - Popup size, available sizes: `giant`, `huge`, `large`, `small`, `tiny`, `adaptive` (will scale to fit the window)
- `data-adaptive-height="false"` - Allow the popup to fill the height of the screen
### JavaScript API
```js
$('a#someLink').popup({ ajax: 'popup-content.htm' })
$('a#someLink').popup({ handler: 'onLoadSomePopup' })
$('a#someLink').popup({ handler: 'onLoadSomePopup', extraData: { id: 3 } })
```

View File

@@ -0,0 +1,9 @@
Progress bar
# Example
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="width: 60%;">
<span class="sr-only">60% Complete</span>
</div>
</div>

View File

@@ -0,0 +1,84 @@
# Scoreboard
### Scoreboard
<div class="scoreboard">
<div data-control="toolbar">
<div class="scoreboard-item title-value">
<h4>Weight</h4>
<p>100</p>
<p class="description">unit: kg</p>
</div>
<div class="scoreboard-item title-value">
<h4>Comments</h4>
<p class="positive">44</p>
<p class="description">previous month: 32</p>
</div>
<div class="scoreboard-item title-value">
<h4>Latest commenter</h4>
<p class="wn-icon-star">John Smith</p>
<p class="description">registered: yes</p>
</div>
</div>
</div>
### Complete example
<div class="scoreboard">
<div data-control="toolbar">
<div class="scoreboard-item control-chart" data-control="chart-pie">
<ul>
<li data-color="#95b753">Published <span>84</span></li>
<li data-color="#e5a91a">Drafts <span>12</span></li>
<li data-color="#cc3300">Deleted <span>18</span></li>
</ul>
</div>
<div class="scoreboard-item control-chart" data-control="chart-bar">
<ul>
<li data-color="#95b753">Published <span>84</span></li>
<li data-color="#e5a91a">Drafts <span>12</span></li>
<li data-color="#cc3300">Deleted <span>18</span></li>
</ul>
</div>
<div class="scoreboard-item title-value">
<h4>Weight</h4>
<p>100</p>
<p class="description">unit: kg</p>
</div>
<div class="scoreboard-item title-value">
<h4>Comments</h4>
<p class="positive">44</p>
<p class="description">previous month: 32</p>
</div>
<div class="scoreboard-item title-value">
<h4>Length</h4>
<p class="negative">31</p>
<p class="description">previous: 42</p>
</div>
<div class="scoreboard-item title-value">
<h4>Latest commenter</h4>
<p class="wn-icon-star">John Smith</p>
<p class="description">registered: yes</p>
</div>
<div class="scoreboard-item title-value" data-control="goal-meter" data-value="88">
<h4>goal meter</h4>
<p>88%</p>
<p class="description">37 posts remain</p>
</div>
<div class="scoreboard-item title-value goal-meter-inverse" data-control="goal-meter" data-value="88">
<h4>goal meter</h4>
<p>88%</p>
<p class="description">37 posts remain</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,156 @@
# Select
### Select
Custom select control.
<select class="form-control custom-select">
<option selected="selected" value="2">Approved</option>
<option value="3">Deleted</option>
<option value="1">New</option>
</select>
## Sizes
### Small size
<div class="form-group form-group-sm">
<select class="form-control custom-select">
<option value="1" selected="selected">One</option>
<option value="2">Two</option>
</select>
</div>
### Large size
<div class="form-group form-group-lg">
<select class="form-control custom-select">
<option value="1" selected="selected">One</option>
<option value="2">Two</option>
</select>
</div>
## Options
### Disable search
Add the `select-no-search` CSS class to disable searching.
<div class="form-group">
<select class="form-control custom-select select-no-search">
<option value="1" selected="selected">One</option>
<option value="2" selected="selected">Two</option>
</select>
</div>
### Dynamic option creation
In addition to a pre-populated menu of options, Select widgets may dynamically create new options from textual input by the user in the search box. This feature is called "tagging". To enable tagging, set the `tags` option to `true`:
<select
class="form-control custom-select"
data-tags="true"
></select>
## Option groups
Use the `optgroup` element to create option groups.
<select class="form-control custom-select">
<option value="1">Please select an option</option>
<option value="2">Ungrouped option</option>
<optgroup label="Option Group">
<option value="3">Grouped option</option>
<option value="4">Another option</option>
<option value="4">Third option</option>
</optgroup>
</select>
## AJAX search
Use the `data-handler` attribute to source the select options from an AJAX handler.
```html
<select
class="form-control custom-select"
data-handler="onGetOptions"
data-minimum-input-length="2"
data-ajax-delay="300"
data-request-data="foo: 'bar'"
></select>
```
The AJAX handler should return results in the [Select2 data format](https://select2.org/data-sources/formats).
```php
public function onGetOptions()
{
return [
'results' => [
[
'id' => 1,
'text' => 'Foo'
],
[
'id' => 2,
'text' => 'Bar'
]
...
]
];
}
```
Or a more full-featured example:
```php
public function onGetOptions()
{
return [
'results' => [
[
'id' => 1,
'text' => 'Foo',
'disabled' => true
],
[
'id' => 2,
'text' => 'Bar',
'selected' => true
],
[
'text' => 'Group',
'children' => [
[
'id' => 3,
'text' => 'Child 1'
],
[
'id' => 4,
'text' => 'Child 2'
]
...
]
]
...
],
'pagination' => [
'more' => true
]
];
}
```
The results array can be assigned to either the `result` or `results` key. As an alternative to the Select2 format, results can also be provided as an associative array (also assigned to either key). Due to the fact that JavaScript does not guarantee the order of object properties, we suggest the method above for defining results.
```php
public function onGetOptions()
{
$results = [
'key' => 'value',
...
];
return ['result' => $results];
}
```

View File

@@ -0,0 +1,7 @@
Includes scaffold for a basic site.
Reset
Normalize
Grid system
Print
Typography

View File

@@ -0,0 +1,155 @@
# Tab control
This plugin is a wrapper for the Twitter Bootstrap Tab component. It provides the following features:
- Adding tabs
- Optional close icons with 2 states (modified / unmodified). The icon state can be changed by triggering the modified.oc.tab/unmodified.oc.tab events on any element within tab, or on the tab itself.
- Removing tabs with the Close icon, or with triggering an event from inside a tab pane or tab. The removing can be canceled if the `confirm.oc.tab` event handler returns `false`.
- Scrolling tabs if they do not fit the screen
- Collapsible tabs
### Supported CSS modifiers
These modifiers can be added in addition to the `control-tabs` class:
- `tabs-inset` - Applies a negative margin to the tabs allowing them to sit well inside a padded container.
- `tabs-offset` - Applies a positive padding to tabs so they sit well inside a flush (non padded) container.
- `tabs-flush` - Tabs to sit flush to the element above it.
### Master tabs
```html
<div class="control-tabs master-tabs" data-control="tab">
<ul class="nav nav-tabs">
<li class="active"><a href="#primaryTabOne">One</a></li>
<li><a href="#primaryTabTwo">Two</a></li>
<li><a href="#primaryTabThree">Three</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active">
Tab one content
</div>
<div class="tab-pane">
Tab two content
</div>
<div class="tab-pane">
Tab three content
</div>
</div>
</div>
```
### Primary tabs
```html
<div class="control-tabs primary-tabs" data-control="tab">
<ul class="nav nav-tabs">
<li class="active"><a href="#primaryTabOne">One</a></li>
<li><a href="#primaryTabTwo">Two</a></li>
<li><a href="#primaryTabThree">Three</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active">
Tab one content
</div>
<div class="tab-pane">
Tab two content
</div>
<div class="tab-pane">
Tab three content
</div>
</div>
</div>
```
> **Note**: Primary tabs in the Winter back-end are inset by default and you should use `.tabs-no-inset` to disable this.
### Secondary tabs
```html
<div class="control-tabs secondary-tabs" data-control="tab">
<ul class="nav nav-tabs">
<li class="active"><a href="#secondaryTabOne">One</a></li>
<li><a href="#secondaryTabTwo">Two</a></li>
<li><a href="#secondaryTabThree">Three</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active">
Tab one content
</div>
<div class="tab-pane">
Tab two content
</div>
<div class="tab-pane">
Tab three content
</div>
</div>
</div>
```
### Content tabs
```html
<div class="control-tabs content-tabs" data-control="tab">
<ul class="nav nav-tabs">
<li class="active"><a href="#contentTabOne">One</a></li>
<li><a href="#contentTabTwo">Two</a></li>
<li><a href="#contentTabThree">Three</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active">
Tab one content
</div>
<div class="tab-pane">
Tab two content
</div>
<div class="tab-pane">
Tab three content
</div>
</div>
</div>
```
### Supported data attributes:
- `data-control="tab"` - creates the tab control from an element
- `data-closable` - enables the Close Tab feature
- `data-pane-classes` - a list of CSS classes to apply new pane elements
Example with data attributes:
```html
<div class="control-tabs master" data-control="tab" data-closable>
<ul class="nav nav-tabs">
<li class="active"><a href="#home">Home</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active">Home</div>
</div>
</div>
```
### JavaScript API
- `$('#mytabs').ocTab({closable: true, closeConfirmation: 'Close this tab? Unsaved data will be lost.'})`
- `$('#mytabs').ocTab('addTab', 'Tab title', 'Tab content', identifier)` - adds tab. The optional identifier parameter allows to associate a identifier with a tab. The identifier can be used with the `goTo()` method to find and open a tab by it's identifier.
- `$('#mytabs').ocTab('closeTab', '.nav-tabs > li.active', true)` - closes a tab. The second argument can point to a tab or tab pane. The thrid argument determines whether the tab should be closed without the user confirmation. The default value is `false`.
- `$('.nav-tabs > li.active').trigger('close.oc.tab')` - another way to close a tab. The event can be triggered on a tab, tab pane or any element inside a tab or tab pane.
- `$('#mytabs').ocTab('modifyTab', '.nav-tabs > li.active')` - marks a tab as modified. Use the `unmodifyTab` to mark a tab as unmodified.
- `$('.nav-tabs > li.active').trigger('modified.oc.tab')` - another way to mark a tab as modified. The event can be triggered on a tab, tab pane or any element inside a tab or tab pane. Use the `unmodified.oc.tab` to mark a tab as unmodified.
- `$('#mytabs').ocTab('goTo', 'someidentifier')` - Finds a tab by it's identifier and opens it.
- `$('#mytabs').ocTab('goToPane', '.tab-content .tab-pane:first')` - Opens a tab in context of it's content (pane element)
### Supported options:
- `closable` - adds the "close" icon to the tab and lets users to close tabs. Corresponds the `data-closable` attribute.
- `closeConfirmation` - a confirmation to show when a user tries to close a modified tab. Corresponds the `data-close-confirmation` attribute. The confirmation is displayed only if the tab was modified.
- `slidable` - allows the tabs to be switched with the swipe gesture on touch devices. Corresponds the `data-slidable` attribute.
- `paneClasses` - a list of CSS classes to apply new pane elements. Corresponds to the `data-pane-classes` attribute.
- `maxTitleSymbols` - the maximum number of characters in tab titles.
- `titleAsFileNames` - treat tab titles as file names. In this mode only the file name part is displayed in the tab, and the directory part is hidden.
### Supported events:
- `beforeClose.oc.tab` - triggered on a tab pane element before tab is closed by the user. Call the event's `preventDefault()` method to cancel the action.
- `afterAllClosed.oc.tab` - triggered after all tabs have been closed

View File

@@ -0,0 +1,162 @@
# Toolbar
A scrollable set of buttons aligned to the left with a fixed right section.
All toolbar items (`toolbar-item`) should have a fixed width, except for the primary item (`toolbar-primary`) which will stretch. In the Winter backend you can use the `data-calculate-width` attribute to have these widths calculated dynamically for you.
## Basic toolbar
<div class="control-toolbar">
<div class="toolbar-item toolbar-primary">
<div data-control="toolbar">
<button type="button" class="btn btn-primary wn-icon-plus">Create post</button>
<button type="button" class="btn btn-default wn-icon-copy">Copy</button>
<button type="button" class="btn btn-default wn-icon-trash">Delete</button>
<button type="button" class="btn btn-default wn-icon-magic">Publish</button>
<button type="button" class="btn btn-default wn-icon-power-off">Unpublish</button>
<button type="button" class="btn btn-default wn-icon-clock-o">Timer</button>
<button type="button" class="btn btn-default wn-icon-mail-reply-all">Send by email</button>
<button type="button" class="btn btn-default wn-icon-hdd-o">Archive</button>
</div>
</div>
<div class="toolbar-item" style="width: 110px">
<input placeholder="search..." type="text" name="" value="" class="form-control icon search" />
</div>
</div>
### Button groups
<div class="control-toolbar">
<div class="toolbar-item toolbar-primary">
<div data-control="toolbar">
<div class="btn-group">
<button type="button" class="btn btn-default wn-icon-mail-reply-all">Send by email</button>
<button type="button" class="btn btn-default wn-icon-hdd-o">Archive</button>
</div>
</div>
</div>
<div class="toolbar-item" style="width: 110px">
<input placeholder="search..." type="text" name="" value="" class="form-control icon search" />
</div>
</div>
### Button with Tooltips
<div class="control-toolbar">
<div class="toolbar-item toolbar-primary">
<div data-control="toolbar">
<button
type="button"
class="btn btn-default wn-icon-download"
title="Hold down shift for more options"
data-control="tooltip"
data-placement="bottom"
data-container="body">
Export
</button>
</div>
</div>
<div class="toolbar-item" style="width: 110px">
<input placeholder="search..." type="text" name="" value="" class="form-control icon search" />
</div>
</div>
### Dropdown buttons
<div class="control-toolbar">
<div class="toolbar-item toolbar-primary">
<div data-control="toolbar">
<div class="dropdown dropdown-fixed">
<button
type="button"
class="btn btn-default wn-icon-users"
data-toggle="dropdown">
Assign selected to...
</button>
<ul class="dropdown-menu" data-dropdown-title="Assign selected to...">
<li><a href="#" tabindex="-1" class="wn-icon-user">Sally</a></li>
<li><a href="#" tabindex="-1" class="wn-icon-user">Steve</a></li>
<li><a href="#" tabindex="-1" class="wn-icon-user">Justin</a></li>
</ul>
</div>
</div>
</div>
<div class="toolbar-item" style="width: 110px">
<input placeholder="search..." type="text" name="" value="" class="form-control icon search" />
</div>
</div>
## Editor toolbar
<div class="layout control-toolbar editor-toolbar">
<div class="layout-cell toolbar-item">
<div data-control="toolbar">
<!-- Dropdown item -->
<div class="dropdown dropdown-fixed">
<button
type="button"
class="btn"
title="Formatting"
data-toggle="dropdown"
data-control="tooltip"
data-placement="bottom"
data-container="body">
<i class="icon-paragraph"></i>
</button>
<ul class="dropdown-menu" data-dropdown-title="Formatting">
<li><a href="#" tabindex="-1" class="wn-icon-quote-right">Quote</a></li>
<li><a href="#" tabindex="-1" class="wn-icon-code">Code</a></li>
<li><a href="#" tabindex="-1" class="wn-icon-header">Header 1</a></li>
<li><a href="#" tabindex="-1" class="wn-icon-header">Header 2</a></li>
<li><a href="#" tabindex="-1" class="wn-icon-header">Header 3</a></li>
<li><a href="#" tabindex="-1" class="wn-icon-header">Header 4</a></li>
<li><a href="#" tabindex="-1" class="wn-icon-header">Header 5</a></li>
<li><a href="#" tabindex="-1" class="wn-icon-header">Header 6</a></li>
</ul>
</div>
<!-- Item with tooltip -->
<button
type="button"
class="btn"
title="Bold"
data-control="tooltip"
data-placement="bottom"
data-container="body">
<i class="icon-bold"></i>
</button>
<!-- Disabled item -->
<button type="button" disabled class="btn">
<i class="icon-italic"></i>
</button>
<button type="button" class="btn">
<i class="icon-list-ul"></i>
</button>
<button type="button" class="btn">
<i class="icon-list-ol"></i>
</button>
<button type="button" class="btn">
<i class="icon-link"></i>
</button>
<button type="button" class="btn">
<i class="icon-minus"></i>
</button>
</div>
</div>
<div class="toolbar-item" style="width: 80px">
<button type="button" class="btn wn-icon-eye"></button>
<button type="button" class="btn wn-icon-expand"></button>
</div>
</div>

View File

@@ -0,0 +1,30 @@
# Tooltips
Tooltips are an alternative to the standard browser title tooltip.
## Tooltip markup
A standard tooltip
<div class="tooltip fade top in">
<div class="tooltip-arrow"></div>
<div class="tooltip-inner">Create a new blog post based on this</div>
</div>
## Spawning tooltips
Tooltips can be automatically created when the mouse enters an element using the `data-toggle="tooltip"` tag.
<a
href="javascript:;"
data-toggle="tooltip"
data-placement="left"
data-delay="500"
title="Tooltip content">
Some link
</a>
# Example
<div class="tooltip fade top in">
<div class="tooltip-arrow"></div>
<div class="tooltip-inner">Create a new blog post based on this</div>
</div>

View File

@@ -0,0 +1,129 @@
Utility styles are a collection of useful classes designed to reduce the need to create a stylesheet for basic styling needs, such as spacing and positioning.
### Branding
```css
.br-p { color: @brand-primary; }
.br-s { color: @brand-secondary; }
.br-a { color: @brand-accent; }
.br-p-s10 { color: saturate(@brand-primary, 10%); }
.br-s-s10 { color: saturate(@brand-secondary, 10%); }
.br-a-s10 { color: saturate(@brand-accent, 10%); }
.br-p-s20 { color: saturate(@brand-primary, 20%); }
.br-s-s20 { color: saturate(@brand-secondary, 20%); }
.br-a-s20 { color: saturate(@brand-accent, 20%); }
.bg-p { background-color: @brand-primary; }
.bg-s { background-color: @brand-secondary; }
.bg-a { background-color: @brand-accent; }
.bg-p-s10 { background-color: saturate(@brand-primary, 10%); }
.bg-s-s10 { background-color: saturate(@brand-secondary, 10%); }
.bg-a-s10 { background-color: saturate(@brand-accent, 10%); }
.bg-p-s20 { background-color: saturate(@brand-primary, 20%); }
.bg-s-s20 { background-color: saturate(@brand-secondary, 20%); }
.bg-a-s20 { background-color: saturate(@brand-accent, 20%); }
```
### Typography
```css
.t-ww { word-wrap: break-word; }
.t-nw { white-space: nowrap; }
```
### Positioning
```css
.pos-r { position: relative !important; }
.pos-a { position: absolute !important; }
.pos-f { position: fixed !important; }
```
### Width
```css
.w-sm { width: 25% !important; }
.w-md { width: 50% !important; }
.w-lg { width: 75% !important; }
.w-full { width: 100% !important; }
.w-100 { width: 100px !important; }
.w-120 { width: 120px !important; }
.w-130 { width: 130px !important; }
.w-140 { width: 140px !important; }
.w-200 { width: 200px !important; }
.w-300 { width: 300px !important; }
.w-350 { width: 350px !important; }
```
### Margin
Assign `margin` to an element with these shorthand classes. The `@spacer` value is set to 20px by default.
```css
.m-a-0 { margin: 0 !important; }
.m-t-0 { margin-top: 0 !important; }
.m-r-0 { margin-right: 0 !important; }
.m-b-0 { margin-bottom: 0 !important; }
.m-l-0 { margin-left: 0 !important; }
.m-a { margin: @spacer !important; }
.m-t { margin-top: @spacer-y !important; }
.m-r { margin-right: @spacer-x !important; }
.m-b { margin-bottom: @spacer-y !important; }
.m-l { margin-left: @spacer-x !important; }
.m-x { margin-right: @spacer-x !important; margin-left: @spacer-x !important; }
.m-y { margin-top: @spacer-y !important; margin-bottom: @spacer-y !important; }
.m-x-auto { margin-right: auto !important; margin-left: auto !important; }
.m-a-md { margin: (@spacer-y * 1.5) !important; }
.m-t-md { margin-top: (@spacer-y * 1.5) !important; }
.m-r-md { margin-right: (@spacer-y * 1.5) !important; }
.m-b-md { margin-bottom: (@spacer-y * 1.5) !important; }
.m-l-md { margin-left: (@spacer-y * 1.5) !important; }
.m-x-md { margin-right: (@spacer-x * 1.5) !important; margin-left: (@spacer-x * 1.5) !important; }
.m-y-md { margin-top: (@spacer-y * 1.5) !important; margin-bottom: (@spacer-y * 1.5) !important; }
.m-a-lg { margin: (@spacer-y * 3) !important; }
.m-t-lg { margin-top: (@spacer-y * 3) !important; }
.m-r-lg { margin-right: (@spacer-y * 3) !important; }
.m-b-lg { margin-bottom: (@spacer-y * 3) !important; }
.m-l-lg { margin-left: (@spacer-y * 3) !important; }
.m-x-lg { margin-right: (@spacer-x * 3) !important; margin-left: (@spacer-x * 3) !important; }
.m-y-lg { margin-top: (@spacer-y * 3) !important; margin-bottom: (@spacer-y * 3) !important; }
```
### Padding
Assign `padding` to an element with these shorthand classes. The `@spacer` value is set to 20px by default.
```css
.p-a-0 { padding: 0 !important; }
.p-t-0 { padding-top: 0 !important; }
.p-r-0 { padding-right: 0 !important; }
.p-b-0 { padding-bottom: 0 !important; }
.p-l-0 { padding-left: 0 !important; }
.p-a { padding: @spacer !important; }
.p-t { padding-top: @spacer-y !important; }
.p-r { padding-right: @spacer-x !important; }
.p-b { padding-bottom: @spacer-y !important; }
.p-l { padding-left: @spacer-x !important; }
.p-x { padding-right: @spacer-x !important; padding-left: @spacer-x !important; }
.p-y { padding-top: @spacer-y !important; padding-bottom: @spacer-y !important; }
.p-a-md { padding: (@spacer-y * 1.5) !important; }
.p-t-md { padding-top: (@spacer-y * 1.5) !important; }
.p-r-md { padding-right: (@spacer-y * 1.5) !important; }
.p-b-md { padding-bottom: (@spacer-y * 1.5) !important; }
.p-l-md { padding-left: (@spacer-y * 1.5) !important; }
.p-x-md { padding-right: (@spacer-x * 1.5) !important; padding-left: (@spacer-x * 1.5) !important; }
.p-y-md { padding-top: (@spacer-y * 1.5) !important; padding-bottom: (@spacer-y * 1.5) !important; }
.p-a-lg { padding: (@spacer-y * 3) !important; }
.p-t-lg { padding-top: (@spacer-y * 3) !important; }
.p-r-lg { padding-right: (@spacer-y * 3) !important; }
.p-b-lg { padding-bottom: (@spacer-y * 3) !important; }
.p-l-lg { padding-left: (@spacer-y * 3) !important; }
.p-x-lg { padding-right: (@spacer-x * 3) !important; padding-left: (@spacer-x * 3) !important; }
.p-y-lg { padding-top: (@spacer-y * 3) !important; padding-bottom: (@spacer-y * 3) !important; }
```

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
// FONT AWESOME 6
// Includes shims for Font Awesome 4 class names
@import "less/icon.less";
@import "less/icon.icons.less";
@import "less/icon.shims.less";

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
x="0px" y="0px" width="41px" height="41px" viewBox="134 73 41 41" enable-background="new 134 73 41 41" xml:space="preserve">
<defs>
</defs>
<rect x="0.5" y="0.5" display="none" fill="#B8CC44" stroke="#FFFFFF" width="315" height="291"/>
<path id="bg_1_" opacity="0" fill="#FFFFFF" d="M135.015,93.554c0.002-10.854,8.8-19.653,19.654-19.655
c10.856,0.002,19.653,8.802,19.655,19.657c-0.001,6.679-3.331,12.577-8.422,16.129c-3.184,2.223-7.056,3.525-11.232,3.526
C143.815,113.209,135.016,104.41,135.015,93.554z"/>
<path opacity="0" fill="none" stroke="#2A98DB" stroke-width="6" d="M137.644,93.554c0.001-9.402,7.623-17.024,17.025-17.026
c9.404,0.002,17.025,7.625,17.026,17.028c0,5.785-2.886,10.896-7.295,13.972c-2.758,1.925-6.112,3.054-9.73,3.055
C145.267,110.58,137.645,102.959,137.644,93.554z"/>
<path fill="none" stroke="#5FB6F5" stroke-width="5.28" stroke-linecap="round" stroke-linejoin="round" d="M142.114,105.059
c-4.002-4.376-5.601-10.721-3.64-16.765c1.318-4.052,4.011-7.271,7.39-9.312"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
x="0px" y="0px" width="41px" height="41px" viewBox="134 73 41 41" enable-background="new 134 73 41 41" xml:space="preserve">
<defs>
</defs>
<rect x="0.5" y="0.5" display="none" fill="#B8CC44" stroke="#FFFFFF" width="315" height="291"/>
<path id="bg_1_" display="none" fill="#FFFFFF" d="M135.015,93.554c0.002-10.854,8.8-19.653,19.654-19.655
c10.856,0.002,19.653,8.802,19.655,19.657c-0.001,6.679-3.331,12.577-8.422,16.129c-3.184,2.223-7.056,3.525-11.232,3.526
C143.815,113.209,135.016,104.41,135.015,93.554z"/>
<path opacity="0" fill="none" stroke="#2A98DB" stroke-width="6" d="M137.644,93.554c0.001-9.402,7.623-17.024,17.025-17.026
c9.404,0.002,17.025,7.625,17.026,17.028c0,5.785-2.886,10.896-7.295,13.972c-2.758,1.925-6.112,3.054-9.73,3.055
C145.267,110.58,137.645,102.959,137.644,93.554z"/>
<path fill="none" stroke="#FFFFFF" stroke-width="5.28" stroke-linecap="round" stroke-linejoin="round" d="M142.114,105.059
c-4.002-4.376-5.601-10.721-3.64-16.765c1.318-4.052,4.011-7.271,7.39-9.312"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
x="0px" y="0px" width="41px" height="41px" viewBox="134 73 41 41" enable-background="new 134 73 41 41" xml:space="preserve">
<defs>
</defs>
<rect x="0.5" y="0.5" display="none" fill="#B8CC44" stroke="#FFFFFF" width="315" height="291"/>
<path id="bg_1_" fill="#FFFFFF" d="M135.015,93.554c0.002-10.854,8.8-19.653,19.654-19.655c10.856,0.002,19.653,8.802,19.655,19.657
c-0.001,6.679-3.331,12.577-8.422,16.129c-3.184,2.223-7.056,3.525-11.232,3.526C143.815,113.209,135.016,104.41,135.015,93.554z"/>
<path opacity="0" fill="none" stroke="#2A98DB" stroke-width="6" d="M137.644,93.554c0.001-9.402,7.623-17.024,17.025-17.026
c9.404,0.002,17.025,7.625,17.026,17.028c0,5.785-2.886,10.896-7.295,13.972c-2.758,1.925-6.112,3.054-9.73,3.055
C145.267,110.58,137.645,102.959,137.644,93.554z"/>
<path fill="none" stroke="#5FB6F5" stroke-width="5.28" stroke-linecap="round" stroke-linejoin="round" d="M142.114,105.059
c-4.002-4.376-5.601-10.721-3.64-16.765c1.318-4.052,4.011-7.271,7.39-9.312"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

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);

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