'wcf.global.page.previous': '{capture assign=pagePrevious}{lang}wcf.global.page.previous{/lang}{/capture}{@$pagePrevious|encodeJS}',
'wcf.global.pageDirection': '{lang}wcf.global.pageDirection{/lang}',
'wcf.global.reason': '{lang}wcf.global.reason{/lang}',
+ 'wcf.global.scrollUp': '{lang}wcf.global.scrollUp{/lang}',
'wcf.global.sidebar.hideLeftSidebar': '{lang}wcf.global.sidebar.hideLeftSidebar{/lang}',
'wcf.global.sidebar.hideRightSidebar': '{lang}wcf.global.sidebar.hideRightSidebar{/lang}',
'wcf.global.sidebar.showLeftSidebar': '{lang}wcf.global.sidebar.showLeftSidebar{/lang}',
'wcf.page.sitemap': '{lang}wcf.page.sitemap{/lang}',
'wcf.style.changeStyle': '{lang}wcf.style.changeStyle{/lang}',
'wcf.user.activityPoint': '{lang}wcf.user.activityPoint{/lang}',
- 'wcf.style.changeStyle': '{lang}wcf.style.changeStyle{/lang}',
'wcf.user.panel.markAllAsRead': '{lang}wcf.user.panel.markAllAsRead{/lang}',
'wcf.user.panel.markAsRead': '{lang}wcf.user.panel.markAsRead{/lang}',
'wcf.user.panel.settings': '{lang}wcf.user.panel.settings{/lang}',
if (this._dropdownContainer === null) {
this._dropdownContainer = $('<div class="dropdownMenuContainer" />').appendTo(document.body);
WCF.CloseOverlayHandler.addCallback('WCF.Dropdown.Interactive.Handler', $.proxy(this.closeAll, this));
+ window.addEventListener('scroll', this.closeAll.bind(this));
}
var $instance = new WCF.Dropdown.Interactive.Instance(this._dropdownContainer, triggerElement, identifier, options);
*/
define(
[
- 'Ajax', 'WoltLab/WCF/Bootstrap', 'WoltLab/WCF/Controller/Sitemap', 'WoltLab/WCF/Controller/Style/Changer',
- 'WoltLab/WCF/Controller/Popover'
+ 'Ajax', 'WoltLab/WCF/Bootstrap', 'WoltLab/WCF/Controller/Sitemap', 'WoltLab/WCF/Controller/Style/Changer',
+ 'WoltLab/WCF/Controller/Popover', 'WoltLab/WCF/Ui/Page/JumpToTop'
],
function(
- Ajax, Bootstrap, ControllerSitemap, ControllerStyleChanger,
- ControllerPopover
+ Ajax, Bootstrap, ControllerSitemap, ControllerStyleChanger,
+ ControllerPopover, UiPageJumpToTop
)
{
"use strict";
this._initUserPopover();
this._invokeBackgroundQueue(options.backgroundQueue.url, options.backgroundQueue.force);
+
+ new UiPageJumpToTop();
},
/**
[
'Ajax', 'Core', 'Dictionary', 'EventHandler',
'Language', 'List', 'ObjectMap', 'Dom/ChangeListener',
- 'Dom/Traverse', 'Dom/Util', 'Ui/Confirmation', 'Ui/SimpleDropdown'
+ 'Dom/Traverse', 'Dom/Util', 'Ui/Confirmation', 'Ui/SimpleDropdown',
+ 'WoltLab/WCF/Ui/Page/Action'
],
function(
Ajax, Core, Dictionary, EventHandler,
Language, List, ObjectMap, DomChangeListener,
- DomTraverse, DomUtil, UiConfirmation, UiSimpleDropdown
+ DomTraverse, DomUtil, UiConfirmation, UiSimpleDropdown,
+ UiPageAction
)
{
"use strict";
var _containers = new Dictionary();
var _editors = new Dictionary();
+ var _editorDropdowns = new Dictionary();
var _elements = elByClass('jsClipboardContainer');
var _itemData = new ObjectMap();
var _knownCheckboxes = new List();
*
* @exports WoltLab/WCF/Controller/Clipboard
*/
- var ControllerClipboard = {
+ return {
/**
* Initializes the clipboard API handler.
*
}
this._initContainers();
- this._initEditors();
if (_options.hasMarkedItems && _elements.length) {
this._loadMarkedItems();
}
DomChangeListener.add('WoltLab/WCF/Controller/Clipboard', this._initContainers.bind(this));
- DomChangeListener.add('WoltLab/WCF/Controller/Clipboard', this._initEditors.bind(this));
},
/**
}
},
- /**
- * Initializes the clipboard editor dropdowns.
- */
- _initEditors: function() {
- var getTypes = function(editor) {
- try {
- var types = elData(editor, 'types');
- if (typeof types === 'string') {
- return JSON.parse('{ "types": ' + types.replace(/'/g, '"') + '}').types;
- }
- }
- catch (e) {
- throw new Error("Expected a valid 'data-type' attribute for element '" + DomUtil.identify(editor) + "'.");
- }
-
- return [];
- };
-
- var editors = elByClass('jsClipboardEditor');
- for (var i = 0, length = editors.length; i < length; i++) {
- var editor = editors[i];
- var types = getTypes(editor);
-
- for (var j = 0, innerLength = types.length; j < innerLength; j++) {
- _editors.set(types[j], editor);
- }
- }
- },
-
/**
* Loads marked items from clipboard.
*/
* Saves the state for given item object ids.
*
* @param {string} type object type
- * @param {array<integer>} objectIds item object ids
- * @param {boolean] isMarked true if marked
+ * @param {array<int>} objectIds item object ids
+ * @param {boolean} isMarked true if marked
*/
_saveState: function(type, objectIds, isMarked) {
Ajax.api(this, {
listItem: listItem,
responseData: null
});
-
- if (typeof window.jQuery === 'function') {
- window.jQuery(_editors.get(type)).trigger('clipboardAction', [ type, data.actionName, data.parameters ]);
- }
};
var confirmMessage = (typeof data.internalData.confirmMessage === 'string') ? data.internalData.confirmMessage : '';
listItem: listItem,
responseData: responseData
});
-
- if (typeof window.jQuery === 'function') {
- window.jQuery(_editors.get(type)).trigger('clipboardActionResponse', [ responseData, type, data.actionName, data.parameters ]);
- }
}
this._loadMarkedItems();
containerData.checkboxes[i].checked = false;
}
- _editors.get(data.returnValues.objectType).innerHTML = '';
+ UiPageAction.remove('wcfClipboard-' + data.returnValues.objectType);
}
}).bind(this));
return;
}
- // clear editors
- _editors.forEach(function(editor) {
- editor.innerHTML = '';
- });
_itemData = new ObjectMap();
// rebuild markings
this._rebuildMarkings(containerData, objectIds);
}).bind(this));
- // no marked items
+ var keepEditors = [], typeName;
+ if (data.returnValues && data.returnValues.items) {
+ for (typeName in data.returnValues.items) {
+ if (data.returnValues.items.hasOwnProperty(typeName)) {
+ keepEditors.push(typeName);
+ }
+ }
+ }
+
+ // clear editors
+ _editors.forEach(function(editor, typeName) {
+ if (keepEditors.indexOf(typeName) === -1) {
+ UiPageAction.remove('wcfClipboard-' + typeName);
+
+ _editorDropdowns.get(typeName).innerHTML = '';
+ }
+ });
+
+ // no items
if (!data.returnValues || !data.returnValues.items) {
return;
}
// rebuild editors
- var fragment = document.createDocumentFragment();
- for (var typeName in data.returnValues.items) {
- if (!data.returnValues.items.hasOwnProperty(typeName) || !_editors.has(typeName)) {
+ var created, dropdown, editor, typeData;
+ var divider, item, itemData, itemIndex, label, unmarkAll;
+ for (typeName in data.returnValues.items) {
+ if (!data.returnValues.items.hasOwnProperty(typeName)) {
continue;
}
- var typeData = data.returnValues.items[typeName];
+ typeData = data.returnValues.items[typeName];
+ created = false;
- var editor = _editors.get(typeName);
- var lists = DomTraverse.childrenByTag(editor, 'UL');
- var list = lists[0] || null;
- if (list === null) {
- list = elCreate('ul');
+ editor = _editors.get(typeName);
+ dropdown = _editorDropdowns.get(typeName);
+ if (editor === undefined) {
+ created = true;
+
+ editor = elCreate('a');
+ editor.className = 'dropdownToggle';
+ editor.textContent = typeData.label;
+
+ _editors.set(typeName, editor);
+
+ dropdown = elCreate('ol');
+ dropdown.className = 'dropdownMenu';
+
+ _editorDropdowns.set(typeName, dropdown);
+ }
+ else {
+ editor.textContent = typeData.label;
}
-
- fragment.appendChild(list);
-
- var listItem = elCreate('li');
- listItem.classList.add('dropdown');
- list.appendChild(listItem);
-
- var toggleButton = elCreate('span');
- toggleButton.className = 'dropdownToggle button';
- toggleButton.textContent = typeData.label;
- listItem.appendChild(toggleButton);
-
- var itemList = elCreate('ol');
- itemList.classList.add('dropdownMenu');
// create editor items
- for (var itemIndex in typeData.items) {
+ for (itemIndex in typeData.items) {
if (!typeData.items.hasOwnProperty(itemIndex)) continue;
- var itemData = typeData.items[itemIndex];
+ itemData = typeData.items[itemIndex];
- var item = elCreate('li');
- var label = elCreate('span');
+ item = elCreate('li');
+ label = elCreate('span');
label.textContent = itemData.label;
item.appendChild(label);
- itemList.appendChild(item);
+ dropdown.appendChild(item);
elData(item, 'type', typeName);
item.addEventListener('click', _callbackItem);
_itemData.set(item, itemData);
}
- var divider = elCreate('li');
+ divider = elCreate('li');
divider.classList.add('dropdownDivider');
- itemList.appendChild(divider);
+ dropdown.appendChild(divider);
// add 'unmark all'
- var unmarkAll = elCreate('li');
+ unmarkAll = elCreate('li');
elData(unmarkAll, 'type', typeName);
- var label = elCreate('span');
+ label = elCreate('span');
label.textContent = Language.get('wcf.clipboard.item.unmarkAll');
unmarkAll.appendChild(label);
- itemList.appendChild(unmarkAll);
- listItem.appendChild(itemList);
-
unmarkAll.addEventListener('click', _callbackUnmarkAll);
- editor.appendChild(fragment);
+ dropdown.appendChild(unmarkAll);
+
+ if (keepEditors.indexOf(typeName) !== -1) {
+ UiPageAction.add('wcfClipboard-' + typeName, editor);
+ }
- UiSimpleDropdown.init(toggleButton, false);
+ if (created) {
+ editor.parentNode.classList.add('dropdown');
+ editor.parentNode.appendChild(dropdown);
+ UiSimpleDropdown.init(editor);
+ }
}
},
* Rebuilds the mark state for each item.
*
* @param {object<string, *>} data container data
- * @param {array<integer>} objectIds item object ids
+ * @param {array<int>} objectIds item object ids
*/
_rebuildMarkings: function(data, objectIds) {
var markAll = true;
}
}
};
-
- return ControllerClipboard;
});
}
_popover = elCreate('div');
- _popover.classList.add('popover');
+ _popover.className = 'popover forceHide';
_popoverContent = elCreate('div');
- _popoverContent.classList.add('popoverContent');
+ _popoverContent.className = 'popoverContent';
_popover.appendChild(_popoverContent);
var pointer = elCreate('span');
- pointer.classList.add('elementPointer');
+ pointer.className = 'elementPointer';
pointer.appendChild(elCreate('span'));
_popover.appendChild(pointer);
return;
}
+ _popover.classList.remove('forceHide');
_popover.classList.add('active');
UiAlignment.set(_popover, _elements.get(_activeId).element, {
*/
_onScroll: function() {
_dropdowns.forEach((function(dropdown, containerId) {
- if (elData(dropdown, 'is-overlay-dropdown-button') === true && dropdown.classList.contains('dropdownOpen')) {
- this.setAlignment(dropdown, _menus.get(containerId));
+ if (dropdown.classList.contains('dropdownOpen')) {
+ if (elDataBool(dropdown, 'is-overlay-dropdown-button')) {
+ this.setAlignment(dropdown, _menus.get(containerId));
+ }
+ else {
+ this.close(containerId);
+ }
}
}).bind(this));
},
--- /dev/null
+/**
+ * Provides page actions such as "jump to top" and clipboard actions.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2016 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLab/WCF/Ui/Page/Action
+ */
+define(['Dictionary', 'Dom/Util'], function(Dictionary, DomUtil) {
+ "use strict";
+
+ var _buttons = new Dictionary();
+ var _container = null;
+ var _didInit = false;
+
+ /**
+ * @exports WoltLab/WCF/Ui/Page/Action
+ */
+ return {
+ /**
+ * Initializes the page action container.
+ */
+ setup: function() {
+ _didInit = true;
+
+ _container = elCreate('ul');
+ _container.className = 'pageAction';
+ document.body.appendChild(_container);
+ },
+
+ /**
+ * Adds a button to the page action list. You can optionally provide a button name to
+ * insert the button right before it. Unmatched button names or empty value will cause
+ * the button to be prepended to the list.
+ *
+ * @param {string} buttonName unique identifier
+ * @param {Element} button button element, must not be wrapped in a <li>
+ * @param {string=} insertBeforeButton insert button before element identified by provided button name
+ */
+ add: function(buttonName, button, insertBeforeButton) {
+ if (_didInit === false) this.setup();
+
+ var listItem = elCreate('li');
+ listItem.appendChild(button);
+ elAttr(listItem, 'aria-hidden', (buttonName === 'toTop' ? 'true' : 'false'));
+ elData(listItem, 'name', buttonName);
+
+ // force 'to top' button to be always at the most outer position
+ if (buttonName === 'toTop') {
+ listItem.className = 'toTop initiallyHidden';
+ _container.appendChild(listItem);
+ }
+ else {
+ var insertBefore = null;
+ if (insertBeforeButton) {
+ insertBefore = _buttons.get(insertBeforeButton);
+ if (insertBefore !== undefined) {
+ insertBefore = insertBefore.parentNode;
+ }
+ }
+
+ if (insertBefore === null && _container.childElementCount) {
+ insertBefore = _container.children[0];
+ }
+
+ if (insertBefore === null) {
+ DomUtil.prepend(listItem, _container);
+ }
+ else {
+ _container.insertBefore(listItem, insertBefore);
+ }
+ }
+
+ _buttons.set(buttonName, button);
+ this._renderContainer();
+ },
+
+ /**
+ * Removes a button by its button name.
+ *
+ * @param {string} buttonName unique identifier
+ */
+ remove: function(buttonName) {
+ var button = _buttons.get(buttonName);
+ if (button !== undefined) {
+ var listItem = button.parentNode;
+ listItem.addEventListener('animationend', function () {
+ _container.removeChild(listItem);
+ _buttons.delete(buttonName);
+ });
+
+ this.hide(buttonName);
+ }
+ },
+
+ /**
+ * Hides a button by its button name.
+ *
+ * @param {string} buttonName unique identifier
+ */
+ hide: function(buttonName) {
+ var button = _buttons.get(buttonName);
+ if (button) {
+ elAttr(button.parentNode, 'aria-hidden', 'true');
+ this._renderContainer();
+ }
+ },
+
+ /**
+ * Shows a button by its button name.
+ *
+ * @param {string} buttonName unique identifier
+ */
+ show: function(buttonName) {
+ var button = _buttons.get(buttonName);
+ if (button) {
+ if (button.parentNode.classList.contains('initiallyHidden')) {
+ button.parentNode.classList.remove('initiallyHidden');
+ }
+
+ elAttr(button.parentNode, 'aria-hidden', 'false');
+ this._renderContainer();
+ }
+ },
+
+ /**
+ * Toggles the container's visibility.
+ *
+ * @protected
+ */
+ _renderContainer: function() {
+ var hasVisibleItems = false;
+ if (_container.childElementCount) {
+ for (var i = 0, length = _container.childElementCount; i < length; i++) {
+ if (elAttr(_container.children[i], 'aria-hidden') === 'false') {
+ hasVisibleItems = true;
+ break;
+ }
+ }
+ }
+
+ _container.classList[(hasVisibleItems ? 'add' : 'remove')]('active');
+ }
+ };
+});
--- /dev/null
+/**
+ * Provides a link to scroll to top once the page is scrolled by at least 50% the height of the window.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2015 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLab/WCF/Ui/Page/JumpToTop
+ */
+define(['Environment', 'Language', './Action'], function(Environment, Language, PageAction) {
+ "use strict";
+
+ /**
+ * @constructor
+ */
+ function JumpToTop() { this.init(); }
+ JumpToTop.prototype = {
+ /**
+ * Initializes the top link for desktop browsers only.
+ */
+ init: function() {
+ // top link is not available on smartphones and tablets (they have a built-in function to accomplish this)
+ if (Environment.platform() !== 'desktop') {
+ return;
+ }
+
+ this._callbackScrollEnd = this._afterScroll.bind(this);
+ this._timeoutScroll = null;
+
+ var button = elCreate('a');
+ button.className = 'jsTooltip';
+ button.href = '#';
+ elAttr(button, 'title', Language.get('wcf.global.scrollUp'));
+ button.innerHTML = '<span class="icon icon16 fa-arrow-up"></span>';
+
+ button.addEventListener('click', this._jump.bind(this));
+
+ PageAction.add('toTop', button);
+
+ window.addEventListener('scroll', this._scroll.bind(this));
+
+ // invoke callback on page load
+ this._afterScroll();
+ },
+
+ /**
+ * Handles clicks on the top link.
+ *
+ * @param {Event} event event object
+ * @protected
+ */
+ _jump: function(event) {
+ event.preventDefault();
+ },
+
+ /**
+ * Callback executed whenever the window is being scrolled.
+ *
+ * @protected
+ */
+ _scroll: function() {
+ if (this._timeoutScroll !== null) {
+ window.clearTimeout(this._timeoutScroll);
+ }
+
+ this._timeoutScroll = window.setTimeout(this._callbackScrollEnd, 100);
+ },
+
+ /**
+ * Delayed callback executed once the page has not been scrolled for a certain amount of time.
+ *
+ * @protected
+ */
+ _afterScroll: function() {
+ this._timeoutScroll = null;
+
+ PageAction[(window.scrollY >= window.innerHeight / 2) ? 'show' : 'hide']('toTop');
+ }
+ };
+
+ return JumpToTop;
+});
this.init();
DomChangeListener.add('WoltLab/WCF/Ui/Tooltip', this.init.bind(this));
+ window.addEventListener('scroll', this._mouseLeave.bind(this));
},
/**
-/* container for both regular and interactive dropdowns */
.dropdownMenuContainer {
- /* force positioning in the upper left corner to prevent elements from
- jumping during calculation */
+ bottom: 0;
left: 0;
+ pointer-events: none;
position: absolute;
right: 0;
top: 0;
-
- > .dropdown {
- /* hide dropdown during calculation */
- left: -200%;
- }
}
.dropdown {
}
&.preInput {
+ // TODO: use flex-box instead
display: table;
width: 100%;
word-wrap: normal;
&.active::after {
- content: "\f0d7"; // @TODO: use a variable instead
+ content: "\f0d7"; // TODO: use a variable instead
font-family: FontAwesome;
font-size: 14px;
margin-left: 7px;
float: left;
min-width: 160px;
padding: 3px 0;
+ pointer-events: all;
position: absolute;
text-align: left;
+ visibility: hidden;
z-index: 450;
@include boxShadow(2px, 2px, rgba(0, 0, 0, .2), 10px);
&.dropdownOpen {
display: block;
+ visibility: visible;
}
li {
-.dropdownMenuContainer > .interactiveDropdown {
- /* hide dropdown during calculation */
- left: -200%;
+.dropdownMenuContainer > .interactiveDropdown.open {
+ visibility: visible;
}
/* styling for interactive dropdowns (currently only used in the user panel) */
box-shadow: 2px 2px 10px 0 rgba(0, 0, 0, .2);
color: $wcfContentText;
display: block;
+ pointer-events: all;
position: absolute;
+ visibility: hidden;
z-index: 450;
- &:not(.open) {
- display: none !important;
- }
-
> .elementPointer {
border: 10px solid transparent;
//border-bottom-color: $wcfDropdownBorder;
--- /dev/null
+@keyframes wcfPageAction {
+ 0% { visibility: visible; transform: translateY(-10px); opacity: 0; }
+ 100% { visibility: visible; transform: translateY(0); opacity: 1; }
+}
+
+@keyframes wcfPageActionOut {
+ 0% { visibility: visible; transform: translateY(0); opacity: 1; }
+ 100% { visibility: hidden; transform: translateY(-10px); opacity: 0; }
+}
+
+@keyframes wcfPageActionRemove {
+ 0% { visibility: visible; transform: translateY(0); opacity: 1; }
+ 60% { visibility: hidden; transform: translateY(-10px); opacity: 0; }
+ 100% { visibility: hidden; transform: translateY(-10px); opacity: 0; max-width: 0; }
+}
+
+.pageAction {
+ bottom: 20px;
+ position: fixed;
+ right: 20px;
+ z-index: 400;
+
+ @extend .inlineList;
+
+ > li {
+ animation: wcfPageActionOut .3s;
+ animation-fill-mode: forwards;
+
+ // required to animate 'max-width' properly when removing items
+ max-width: 400px;
+ white-space: nowrap;
+
+ &[aria-hidden="false"] {
+ animation: wcfPageAction .3s;
+ animation-fill-mode: forwards;
+ }
+
+ &.remove {
+ animation: wcfPageActionRemove .5s;
+ animation-fill-mode: forwards;
+ }
+
+ &.initiallyHidden {
+ animation: 0;
+ visibility: hidden;
+ }
+
+ > a {
+ background-color: rgba(0, 0, 0, .6);
+ color: rgba(224, 224, 224, 1);
+ padding: 5px 10px;
+
+ > .icon {
+ color: rgba(224, 224, 224, 1);
+ }
+ }
+
+ > a:hover,
+ &.dropdownOpen > a {
+ background-color: rgba(0, 0, 0, 1);
+ color: rgba(255, 255, 255, 1);
+
+ > .icon {
+ color: rgba(255, 255, 255, 1);
+ }
+ }
+ }
+}
position: absolute;
top: 0;
vertical-align: middle;
+ visibility: hidden;
width: 400px !important;
z-index: 500;