--- /dev/null
+{if $userProfile === null}
+ {* user no longer exists, use plain output rather than using a broken link *}
+ @{$username}
+{else}
+ {* non-breaking space below to prevent wrapping of user avatar and username *}
+ <a href="{link controller='User' object=$userProfile->getDecoratedObject()}{/link}">{@$userProfile->getAvatar()->getImageTag(16)} {$userProfile->username}</a>
+{/if}
\ No newline at end of file
+<style>
+ woltlab-mention {
+ background-color: rgb(240, 248, 255);
+ border: 1px solid rgb(52, 152, 219);
+ display: inline-block;
+ margin: 0 3px;
+ padding: 0 2px;
+ }
+</style>
<script data-relocate="true">
(function() {
var buttons = ['format', 'wcfSeparator', 'bold', 'italic', 'underline', 'deleted', 'wcfSeparator', 'lists', 'image', 'link'];
}
};
+ // user mentions
+ if (elDataBool(element, 'support-mention')) {
+ config.plugins.push('WoltLabMention');
+ }
+
$(element).redactor(config);
});
'{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabButton.js?v={@LAST_UPDATE_TIME}',
'{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabDropdown.js?v={@LAST_UPDATE_TIME}',
'{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabEvent.js?v={@LAST_UPDATE_TIME}',
+ '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabMention.js?v={@LAST_UPDATE_TIME}',
'{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabQuote.js?v={@LAST_UPDATE_TIME}'
], function() {
return {
init: function() {
+ this._callbacks = [];
+ this._elementId = this.$element[0].id;
+
require(['EventHandler'], this.WoltLabEvent._setEvents.bind(this));
},
editor: this.$editor[0]
});
}).bind(this);
+
+ this.opts.callbacks.keyup = function(event) {
+ var data = {
+ cancel: false,
+ event: event
+ };
+
+ EventHandler.fire('com.woltlab.wcf.redactor', 'keyup_' + elementId, data);
+
+ return (data.cancel === false);
+ };
+ },
+
+ register: function(callbackName, callback) {
+ require(['EventHandler'], (function(EventHandler) {
+ if (this._callbacks.indexOf(callbackName) === -1) {
+ this.opts.callbacks[callbackName] = (function (event) {
+ var data = {
+ cancel: false,
+ event: event,
+ redactor: this
+ };
+
+ EventHandler.fire('com.woltlab.wcf.redactor2', callbackName + '_' + this.WoltLabEvent._elementId, data);
+
+ return (data.cancel === false);
+ }).bind(this);
+
+ this._callbacks.push(callbackName);
+ }
+
+ require(['EventHandler'], (function(EventHandler) {
+ EventHandler.add('com.woltlab.wcf.redactor2', callbackName + '_' + this.WoltLabEvent._elementId, callback);
+ }).bind(this));
+ }).bind(this));
}
};
};
--- /dev/null
+$.Redactor.prototype.WoltLabMention = function() {
+ "use strict";
+
+ return {
+ init: function() {
+ var WoltLabMention = document.registerElement('woltlab-mention');
+
+ require(['WoltLab/WCF/Ui/Redactor/Mention'], (function(UiRedactorMention) {
+ new UiRedactorMention(this);
+ }).bind(this));
+ }
+ };
+};
* http://flaviusmatis.github.com/license.html
*/
(function(e){var t={init:function(){var t=["paddingTop","paddingRight","paddingBottom","paddingLeft","fontSize","lineHeight","fontFamily","width","fontWeight","border-top-width","border-right-width","border-bottom-width","border-left-width","-moz-box-sizing","-webkit-box-sizing","box-sizing"];return this.each(function(){function i(){for(var e=0;e<t.length;e++){r.css(t[e],n.css(t[e]))}}function c(){var e=n.val().replace(/</g,"<").replace(/>/g,">").replace(/&/g,"&").replace(/\n/g,"<br/>");r.html(e+" ");h()}function h(){var e=r.height();var t="hidden";var i=s?e+a+o:e+a;if(i>l){i=l;t="auto"}else if(i<f){i=f}if(n.height()!==i){n.css({overflow:t,height:i+"px"})}}if(this.type!=="textarea")return false;var n=e(this).css({resize:"none",overflow:"hidden"});var r=e("<div></div>").css({position:"absolute",display:"none","word-wrap":"break-word","white-space":"pre-wrap","border-style":"solid"}).appendTo(document.body);i();var s=n.css("box-sizing")=="border-box"||n.css("-moz-box-sizing")=="border-box"||n.css("-webkit-box-sizing")=="border-box";var o=parseInt(n.css("border-top-width"))+parseInt(n.css("padding-top"))+parseInt(n.css("padding-bottom"))+parseInt(n.css("border-bottom-width"));var u=parseInt(n.css("height"),10);var a=parseInt(n.css("line-height"),10)||parseInt(n.css("font-size"),10);var f=a*2>u?a*2:u;var l=parseInt(n.css("max-height"),10)>-1?parseInt(n.css("max-height"),10):Number.MAX_VALUE;n.bind("keyup change cut paste",function(){c()});e(window).bind("resize",function(){var e=parseInt(n.width(),10);if(r.width()!==e){r.css({width:e+"px"});c()}});n.bind("blur",function(){h()});n.bind("updateHeight",function(){i();c()});e(function(){c()})})}};e.fn.flexible=function(n){if(t[n]){return t[n].apply(this,Array.prototype.slice.call(arguments,1))}else if(typeof n==="object"||!n){return t.init.apply(this,arguments)}else{e.error("Method "+n+" does not exist on jQuery.flexible")}}})(jQuery);
+
+/*! (C) WebReflection Mit Style License */
+(function(e,t,n,r){"use strict";function rt(e,t){for(var n=0,r=e.length;n<r;n++)dt(e[n],t)}function it(e){for(var t=0,n=e.length,r;t<n;t++)r=e[t],nt(r,b[ot(r)])}function st(e){return function(t){j(t)&&(dt(t,e),rt(t.querySelectorAll(w),e))}}function ot(e){var t=e.getAttribute("is"),n=e.nodeName.toUpperCase(),r=S.call(y,t?v+t.toUpperCase():d+n);return t&&-1<r&&!ut(n,t)?-1:r}function ut(e,t){return-1<w.indexOf(e+'[is="'+t+'"]')}function at(e){var t=e.currentTarget,n=e.attrChange,r=e.attrName,i=e.target;Q&&(!i||i===t)&&t.attributeChangedCallback&&r!=="style"&&t.attributeChangedCallback(r,n===e[a]?null:e.prevValue,n===e[l]?null:e.newValue)}function ft(e){var t=st(e);return function(e){X.push(t,e.target)}}function lt(e){K&&(K=!1,e.currentTarget.removeEventListener(h,lt)),rt((e.target||t).querySelectorAll(w),e.detail===o?o:s),B&&pt()}function ct(e,t){var n=this;q.call(n,e,t),G.call(n,{target:n})}function ht(e,t){D(e,t),et?et.observe(e,z):(J&&(e.setAttribute=ct,e[i]=Z(e),e.addEventListener(p,G)),e.addEventListener(c,at)),e.createdCallback&&Q&&(e.created=!0,e.createdCallback(),e.created=!1)}function pt(){for(var e,t=0,n=F.length;t<n;t++)e=F[t],E.contains(e)||(F.splice(t,1),dt(e,o))}function dt(e,t){var n,r=ot(e);-1<r&&(tt(e,b[r]),r=0,t===s&&!e[s]?(e[o]=!1,e[s]=!0,r=1,B&&S.call(F,e)<0&&F.push(e)):t===o&&!e[o]&&(e[s]=!1,e[o]=!0,r=1),r&&(n=e[t+"Callback"])&&n.call(e))}if(r in t)return;var i="__"+r+(Math.random()*1e5>>0),s="attached",o="detached",u="extends",a="ADDITION",f="MODIFICATION",l="REMOVAL",c="DOMAttrModified",h="DOMContentLoaded",p="DOMSubtreeModified",d="<",v="=",m=/^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+$/,g=["ANNOTATION-XML","COLOR-PROFILE","FONT-FACE","FONT-FACE-SRC","FONT-FACE-URI","FONT-FACE-FORMAT","FONT-FACE-NAME","MISSING-GLYPH"],y=[],b=[],w="",E=t.documentElement,S=y.indexOf||function(e){for(var t=this.length;t--&&this[t]!==e;);return t},x=n.prototype,T=x.hasOwnProperty,N=x.isPrototypeOf,C=n.defineProperty,k=n.getOwnPropertyDescriptor,L=n.getOwnPropertyNames,A=n.getPrototypeOf,O=n.setPrototypeOf,M=!!n.__proto__,_=n.create||function vt(e){return e?(vt.prototype=e,new vt):this},D=O||(M?function(e,t){return e.__proto__=t,e}:L&&k?function(){function e(e,t){for(var n,r=L(t),i=0,s=r.length;i<s;i++)n=r[i],T.call(e,n)||C(e,n,k(t,n))}return function(t,n){do e(t,n);while((n=A(n))&&!N.call(n,t));return t}}():function(e,t){for(var n in t)e[n]=t[n];return e}),P=e.MutationObserver||e.WebKitMutationObserver,H=(e.HTMLElement||e.Element||e.Node).prototype,B=!N.call(H,E),j=B?function(e){return e.nodeType===1}:function(e){return N.call(H,e)},F=B&&[],I=H.cloneNode,q=H.setAttribute,R=H.removeAttribute,U=t.createElement,z=P&&{attributes:!0,characterData:!0,attributeOldValue:!0},W=P||function(e){J=!1,E.removeEventListener(c,W)},X,V=e.requestAnimationFrame||e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.msRequestAnimationFrame||function(e){setTimeout(e,10)},$=!1,J=!0,K=!0,Q=!0,G,Y,Z,et,tt,nt;O||M?(tt=function(e,t){N.call(t,e)||ht(e,t)},nt=ht):(tt=function(e,t){e[i]||(e[i]=n(!0),ht(e,t))},nt=tt),B?(J=!1,function(){var e=k(H,"addEventListener"),t=e.value,n=function(e){var t=new CustomEvent(c,{bubbles:!0});t.attrName=e,t.prevValue=this.getAttribute(e),t.newValue=null,t[l]=t.attrChange=2,R.call(this,e),this.dispatchEvent(t)},r=function(e,t){var n=this.hasAttribute(e),r=n&&this.getAttribute(e),i=new CustomEvent(c,{bubbles:!0});q.call(this,e,t),i.attrName=e,i.prevValue=n?r:null,i.newValue=t,n?i[f]=i.attrChange=1:i[a]=i.attrChange=0,this.dispatchEvent(i)},s=function(e){var t=e.currentTarget,n=t[i],r=e.propertyName,s;n.hasOwnProperty(r)&&(n=n[r],s=new CustomEvent(c,{bubbles:!0}),s.attrName=n.name,s.prevValue=n.value||null,s.newValue=n.value=t[r]||null,s.prevValue==null?s[a]=s.attrChange=0:s[f]=s.attrChange=1,t.dispatchEvent(s))};e.value=function(e,o,u){e===c&&this.attributeChangedCallback&&this.setAttribute!==r&&(this[i]={className:{name:"class",value:this.className}},this.setAttribute=r,this.removeAttribute=n,t.call(this,"propertychange",s)),t.call(this,e,o,u)},C(H,"addEventListener",e)}()):P||(E.addEventListener(c,W),E.setAttribute(i,1),E.removeAttribute(i),J&&(G=function(e){var t=this,n,r,s;if(t===e.target){n=t[i],t[i]=r=Z(t);for(s in r){if(!(s in n))return Y(0,t,s,n[s],r[s],a);if(r[s]!==n[s])return Y(1,t,s,n[s],r[s],f)}for(s in n)if(!(s in r))return Y(2,t,s,n[s],r[s],l)}},Y=function(e,t,n,r,i,s){var o={attrChange:e,currentTarget:t,attrName:n,prevValue:r,newValue:i};o[s]=e,at(o)},Z=function(e){for(var t,n,r={},i=e.attributes,s=0,o=i.length;s<o;s++)t=i[s],n=t.name,n!=="setAttribute"&&(r[n]=t.value);return r})),t[r]=function(n,r){p=n.toUpperCase(),$||($=!0,P?(et=function(e,t){function n(e,t){for(var n=0,r=e.length;n<r;t(e[n++]));}return new P(function(r){for(var i,s,o=0,u=r.length;o<u;o++)i=r[o],i.type==="childList"?(n(i.addedNodes,e),n(i.removedNodes,t)):(s=i.target,Q&&s.attributeChangedCallback&&i.attributeName!=="style"&&s.attributeChangedCallback(i.attributeName,i.oldValue,s.getAttribute(i.attributeName)))})}(st(s),st(o)),et.observe(t,{childList:!0,subtree:!0})):(X=[],V(function E(){while(X.length)X.shift().call(null,X.shift());V(E)}),t.addEventListener("DOMNodeInserted",ft(s)),t.addEventListener("DOMNodeRemoved",ft(o))),t.addEventListener(h,lt),t.addEventListener("readystatechange",lt),t.createElement=function(e,n){var r=U.apply(t,arguments),i=""+e,s=S.call(y,(n?v:d)+(n||i).toUpperCase()),o=-1<s;return n&&(r.setAttribute("is",n=n.toLowerCase()),o&&(o=ut(i.toUpperCase(),n))),Q=!t.createElement.innerHTMLHelper,o&&nt(r,b[s]),r},H.cloneNode=function(e){var t=I.call(this,!!e),n=ot(t);return-1<n&&nt(t,b[n]),e&&it(t.querySelectorAll(w)),t});if(-2<S.call(y,v+p)+S.call(y,d+p))throw new Error("A "+n+" type is already registered");if(!m.test(p)||-1<S.call(g,p))throw new Error("The type "+n+" is invalid");var i=function(){return f?t.createElement(l,p):t.createElement(l)},a=r||x,f=T.call(a,u),l=f?r[u].toUpperCase():p,c=y.push((f?v:d)+p)-1,p;return w=w.concat(w.length?",":"",f?l+'[is="'+n.toLowerCase()+'"]':l),i.prototype=b[c]=T.call(a,"prototype")?a.prototype:_(H),rt(t.querySelectorAll(w),s),i}})(window,document,Object,"registerElement");
\ No newline at end of file
--- /dev/null
+define(['Ajax', 'Environment', 'EventHandler', 'Ui/Alignment'], function(Ajax, Environment, EventHandler, UiAlignment) {
+ "use strict";
+
+ function UiRedactorMention(redactor) { this.init(redactor); }
+ UiRedactorMention.prototype = {
+ init: function(redactor) {
+ this._active = false;
+ this._caret = null;
+ this._dropdownActive = false;
+ this._dropdownMenu = null;
+ this._itemIndex = 0;
+ this._lineHeight = null;
+ this._mentionStart = '';
+ this._redactor = redactor;
+ this._timer = null;
+
+ redactor.WoltLabEvent.register('keydown', this._keyDown.bind(this));
+ redactor.WoltLabEvent.register('keyup', this._keyUp.bind(this));
+ },
+
+ _keyDown: function(data) {
+ if (!this._dropdownActive) {
+ return;
+ }
+
+ /** @var Event event */
+ var event = data.event;
+
+ switch (event.which) {
+ // enter
+ case 13:
+ this._setUsername(null, this._dropdownMenu.children[this._itemIndex].children[0]);
+ break;
+
+ // arrow up
+ case 38:
+ this._selectItem(-1);
+ break;
+
+ // arrow down
+ case 40:
+ this._selectItem(1);
+ break;
+
+ default:
+ return;
+ break;
+ }
+
+ event.preventDefault();
+ data.cancel = true;
+ },
+
+ _keyUp: function(data) {
+ /** @var Event event */
+ var event = data.event;
+
+ // ignore return key
+ if (event.which === 13) {
+ this._active = false;
+
+ return;
+ }
+
+ var text = this._getTextLineInFrontOfCaret();
+ if (text.length) {
+ var match = text.match(/@([^,]{3,})$/);
+ if (match) {
+ // if mentioning is at text begin or there's a whitespace character
+ // before the '@', everything is fine
+ if (!match.index || text[match.index - 1].match(/\s/)) {
+ this._mentionStart = match[1];
+
+ if (this._timer !== null) {
+ window.clearTimeout(this._timer);
+ this._timer = null;
+ }
+
+ this._timer = window.setTimeout((function() {
+ Ajax.api(this, {
+ parameters: {
+ data: {
+ searchString: this._mentionStart
+ }
+ }
+ });
+
+ this._timer = null;
+ }).bind(this), 500);
+ }
+ }
+ else {
+ this._hideDropdown();
+ }
+ }
+ else {
+ this._hideDropdown();
+ }
+ },
+
+ _setUsername: function(event, item) {
+ if (event) {
+ event.preventDefault();
+ item = event.currentTarget;
+ }
+
+ /*if (this._timer !== null) {
+ this._timer.stop();
+ this._timer = null;
+ }
+ this._proxy.abortPrevious();*/
+
+ var selection = window.getSelection();
+
+ // restore caret position
+ selection.removeAllRanges();
+ selection.addRange(this._caret);
+
+ var orgRange = selection.getRangeAt(0).cloneRange();
+
+ // allow redactor to undo this
+ this._redactor.buffer.set();
+
+ var startContainer = orgRange.startContainer;
+ var startOffset = orgRange.startOffset - (this._mentionStart.length + 1);
+
+ // navigating with the keyboard before hitting enter will cause the text node to be split
+ if (startOffset < 0) {
+ startContainer = startContainer.previousSibling;
+ startOffset = startContainer.length - (this._mentionStart.length + 1) - (orgRange.startOffset - 1);
+ }
+
+ var newRange = document.createRange();
+ newRange.setStart(startContainer, startOffset);
+ newRange.setEnd(orgRange.startContainer, orgRange.startOffset);
+
+ selection.removeAllRanges();
+ selection.addRange(newRange);
+
+ var range = getSelection().getRangeAt(0);
+ range.deleteContents();
+ range.collapse(true);
+
+ var mention = elCreate('woltlab-mention');
+ elAttr(mention, 'contenteditable', 'false');
+ elData(mention, 'user-id', elData(item, 'user-id'));
+ elData(mention, 'username', elData(item, 'username'));
+ mention.textContent = elData(item, 'username');
+
+ // U+200C = zero width non-joiner
+ var text = document.createTextNode('\u200c');
+
+ range.insertNode(text);
+ range.insertNode(mention);
+
+ newRange = document.createRange();
+ newRange.selectNode(text);
+ newRange.collapse(false);
+
+ selection.removeAllRanges();
+ selection.addRange(newRange);
+
+ this._redactor.selection.save();
+
+ this._hideDropdown();
+ },
+
+ _getTextLineInFrontOfCaret: function() {
+ /** @var Range range */
+ var range = window.getSelection().getRangeAt(0);
+ if (!range.collapsed) {
+ return '';
+ }
+
+ // in Firefox, blurring and refocusing the browser creates separate text nodes
+ if (Environment.browser() === 'firefox' && range.startContainer.nodeType === Node.TEXT_NODE) {
+ range.startContainer.parentNode.normalize();
+ }
+
+ var text = range.startContainer.textContent.substr(0, range.startOffset);
+
+ // remove unicode zero-width space and non-breaking space
+ var textBackup = text;
+ text = '';
+ var hadSpace = false;
+ for (var i = 0; i < textBackup.length; i++) {
+ var byte = textBackup.charCodeAt(i).toString(16);
+ if (byte !== '200b' && (!/\s/.test(textBackup[i]) || ((byte === 'a0' || byte === '20') && !hadSpace))) {
+ if (byte === 'a0' || byte === '20') {
+ hadSpace = true;
+ }
+
+ if (textBackup[i] === '@' && i && /\s/.test(textBackup[i - 1])) {
+ hadSpace = false;
+ text = '';
+ }
+
+ text += textBackup[i];
+ }
+ else {
+ hadSpace = false;
+ text = '';
+ }
+ }
+
+ return text;
+ },
+
+ _ajaxSetup: function() {
+ return {
+ data: {
+ actionName: 'getSearchResultList',
+ className: 'wcf\\data\\user\\UserAction',
+ interfaceName: 'wcf\\data\\ISearchAction',
+ parameters: {
+ data: {
+ includeUserGroups: false
+ }
+ }
+ }
+ };
+ },
+
+ _ajaxSuccess: function(data) {
+ if (!Array.isArray(data.returnValues) || !data.returnValues.length) {
+ this._hideDropdown();
+
+ return;
+ }
+
+ if (this._dropdownMenu === null) {
+ this._dropdownMenu = elCreate('ol');
+ this._dropdownMenu.className = 'dropdownMenu';
+ elById('dropdownMenuContainer').appendChild(this._dropdownMenu);
+ }
+
+ this._dropdownMenu.innerHTML = '';
+
+ var callbackClick = this._setUsername.bind(this), link, listItem, user;
+ for (var i = 0, length = data.returnValues.length; i < length; i++) {
+ user = data.returnValues[i];
+
+ listItem = elCreate('li');
+ link = elCreate('a');
+ link.addEventListener('click', callbackClick);
+ link.className = 'box16';
+ link.innerHTML = '<span class="framed">' + user.icon + '</span> <span>' + user.label + '</span>';
+ elData(link, 'user-id', user.objectID);
+ elData(link, 'username', user.label);
+
+ listItem.appendChild(link);
+ this._dropdownMenu.appendChild(listItem);
+ }
+
+ this._dropdownMenu.classList.add('dropdownOpen');
+ this._dropdownActive = true;
+
+ this._updateDropdownPosition();
+ },
+
+ _getDropdownMenuPosition: function() {
+ this._redactor.selection.save();
+
+ var selection = window.getSelection();
+ var orgRange = selection.getRangeAt(0).cloneRange();
+
+ // mark the entire text, starting from the '@' to the current cursor position
+ var newRange = document.createRange();
+ newRange.setStart(orgRange.startContainer, orgRange.startOffset - (this._mentionStart.length + 1));
+ newRange.setEnd(orgRange.startContainer, orgRange.startOffset);
+
+ selection.removeAllRanges();
+ selection.addRange(newRange);
+
+ // get the offsets of the bounding box of current text selection
+ var rect = selection.getRangeAt(0).getBoundingClientRect();
+ var offsets = {
+ top: Math.round(rect.bottom) + document.body.scrollTop,
+ left: Math.round(rect.left) + document.body.scrollLeft
+ };
+
+ if (this._lineHeight === null) {
+ this._lineHeight = Math.round(rect.bottom - rect.top - document.body.scrollTop);
+ }
+
+ // restore caret position
+ this._redactor.selection.restore();
+
+ this._caret = orgRange;
+
+ return offsets;
+ },
+
+ _updateDropdownPosition: function() {
+ try {
+ var offset = this._getDropdownMenuPosition();
+ offset.top += 7; // add a little vertical gap
+
+ this._dropdownMenu.style.setProperty('left', offset.left + 'px', '');
+ this._dropdownMenu.style.setProperty('top', offset.top + 'px', '');
+
+ this._selectItem(0);
+
+ if (offset.top + this._dropdownMenu.offsetHeight + 10 > window.innerHeight + document.body.scrollTop) {
+ this._dropdownMenu.classList.add('dropdownArrowBottom');
+
+ this._dropdownMenu.style.setProperty('top', offset.top - this._dropdownMenu.offsetHeight - 2 * this._lineHeight + 7 + 'px', '');
+ }
+ else {
+ this._dropdownMenu.classList.remove('dropdownArrowBottom');
+ }
+ }
+ catch (e) {
+ console.debug(e);
+ // ignore errors that are caused by pressing enter to
+ // often in a short period of time
+ }
+ },
+
+ _selectItem: function(step) {
+ // find currently active item
+ var item = elBySel('.active', this._dropdownMenu);
+ if (item !== null) {
+ item.classList.remove('active');
+ }
+
+ this._itemIndex += step;
+ if (this._itemIndex === -1) {
+ this._itemIndex = this._dropdownMenu.childElementCount - 1;
+ }
+ else if (this._itemIndex === this._dropdownMenu.childElementCount) {
+ this._itemIndex = 0;
+ }
+
+ this._dropdownMenu.children[this._itemIndex].classList.add('active');
+ },
+
+ _hideDropdown: function() {
+ if (this._dropdownMenu !== null) this._dropdownMenu.classList.remove('dropdownOpen');
+ this._dropdownActive = false;
+ }
+ };
+
+ return UiRedactorMention;
+});
use wcf\system\html\input\filter\IHtmlInputFilter;
use wcf\system\html\input\filter\MessageHtmlInputFilter;
+use wcf\system\html\input\node\HtmlInputNodeProcessor;
use wcf\system\WCF;
class HtmlInputProcessor {
*/
protected $htmlInputFilter;
+ /**
+ * @var HtmlInputNodeProcessor
+ */
+ protected $htmlInputNodeProcessor;
+
public function process($html) {
// filter HTML
- return $this->getHtmlInputFilter()->apply($html);
+ $html = $this->getHtmlInputFilter()->apply($html);
+ // pre-parse HTML
+ $this->getHtmlInputNodeProcessor()->load($html);
+ $this->getHtmlInputNodeProcessor()->process();
+
+ return $this->getHtmlInputNodeProcessor()->getHtml();
}
public function setHtmlInputFilter(IHtmlInputFilter $htmlInputFilter) {
}
/**
- * @return IHtmlInputFilter
+ * @return IHtmlInputFilter|MessageHtmlInputFilter
+ * @throws \DI\NotFoundException
*/
public function getHtmlInputFilter() {
if ($this->htmlInputFilter === null) {
return $this->htmlInputFilter;
}
+
+ public function setHtmlInputNodeProcessor(HtmlInputNodeProcessor $htmlInputNodeProcessor) {
+ $this->htmlInputNodeProcessor = $htmlInputNodeProcessor;
+ }
+
+ /**
+ * @return HtmlInputNodeProcessor
+ * @throws \DI\NotFoundException
+ */
+ public function getHtmlInputNodeProcessor() {
+ if ($this->htmlInputNodeProcessor === null) {
+ $this->htmlInputNodeProcessor = WCF::getDIContainer()->make(HtmlInputNodeProcessor::class);
+ }
+
+ return $this->htmlInputNodeProcessor;
+ }
}
}
protected function setAttributeDefinitions(\HTMLPurifier_Config $config) {
+ // TODO: move this into own PHP classes
$definition = $config->getHTMLDefinition(true);
$definition->addAttribute('blockquote', 'data-quote-title', 'Text');
$definition->addAttribute('blockquote', 'data-quote-url', 'URI');
+
+ $definition->addElement('woltlab-mention', 'Inline', 'Inline', '', [
+ 'data-user-id' => 'Number',
+ 'data-username' => 'Text'
+ ]);
}
}
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\system\html\input\node;
+
+use wcf\system\html\node\HtmlNodeProcessor;
+use wcf\system\WCF;
+
+class HtmlInputNodeProcessor extends HtmlNodeProcessor {
+ public function load($html) {
+ parent::load($html);
+
+ $this->nodeData = [];
+ }
+
+ public function process() {
+ $woltlabMention = WCF::getDIContainer()->get(HtmlInputNodeWoltlabMention::class);
+ $woltlabMention->process($this);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\html\input\node;
+
+use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
+
+class HtmlInputNodeWoltlabMention implements IHtmlInputNode {
+ /**
+ * @var MessageEmbeddedObjectManager
+ */
+ protected $messageEmbeddedObjectManager;
+
+ public function __construct(MessageEmbeddedObjectManager $messageEmbeddedObjectManager) {
+ $this->messageEmbeddedObjectManager = $messageEmbeddedObjectManager;
+ }
+
+ public function process(HtmlInputNodeProcessor $htmlInputNodeProcessor) {
+ $userIds = [];
+
+ /** @var \DOMElement $mention */
+ foreach ($htmlInputNodeProcessor->getDocument()->getElementsByTagName('woltlab-mention') as $mention) {
+ $userId = intval($mention->getAttribute('data-user-id'));
+ if ($userId) {
+ $userIds[] = $userId;
+ }
+ }
+
+ if (!empty($userIds)) {
+
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\html\input\node;
+
+interface IHtmlInputNode {
+ public function process(HtmlInputNodeProcessor $htmlInputNodeProcessor);
+}
--- /dev/null
+<?php
+namespace wcf\system\html\node;
+
+class HtmlNodeProcessor {
+ /**
+ * @var \DOMDocument
+ */
+ protected $document;
+
+ public function load($html) {
+ $this->document = new \DOMDocument();
+
+ // convert entities as DOMDocument screws them up
+ $html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
+
+ // ignore all errors when loading the HTML string, because DOMDocument does not
+ // provide a proper way to add custom HTML elements (even though explicitly allowed
+ // in HTML5) and the input HTML has already been sanitized by HTMLPurifier
+ @$this->document->loadHTML($html);
+ }
+
+ public function getHtml() {
+ $html = $this->document->saveHTML();
+
+ // remove nuisance added by PHP
+ $html = preg_replace('~^<!DOCTYPE[^>]+>\s<html><body>~', '', $html);
+ $html = preg_replace('~</body></html>$~', '', $html);
+
+ $html = mb_convert_encoding($html, 'UTF-8', 'HTML-ENTITIES');
+
+ return $html;
+ }
+
+ public function getDocument() {
+ return $this->document;
+ }
+
+ public function renameTag(\DOMElement $element, $tagName) {
+ $newElement = $this->document->createElement($tagName);
+ $element->parentNode->insertBefore($newElement, $element);
+ while ($element->hasChildNodes()) {
+ $newElement->appendChild($element->firstChild);
+ }
+
+ $element->parentNode->removeChild($element);
+
+ return $newElement;
+ }
+
+ public function unwrapContent(\DOMElement $element) {
+ while ($element->hasChildNodes()) {
+ $element->parentNode->insertBefore($element->firstChild, $element);
+ }
+
+ $element->parentNode->removeChild($element);
+ }
+}
\ No newline at end of file
<?php
namespace wcf\system\html\output;
+use wcf\system\html\node\HtmlNodeProcessor;
+use wcf\system\html\output\node\HtmlOutputNodeBlockquote;
+use wcf\system\html\output\node\HtmlOutputNodeWoltlabMention;
use wcf\system\html\output\node\IHtmlOutputNode;
-use wcf\system\html\output\node\QuoteHtmlOutputNode;
use wcf\system\WCF;
-class HtmlOutputNodeProcessor {
- /**
- * @var \DOMDocument
- */
- protected $document;
-
+class HtmlOutputNodeProcessor extends HtmlNodeProcessor {
protected $nodeData = [];
public function load($html) {
- $this->document = new \DOMDocument();
- $this->document->loadHTML($html);
+ parent::load($html);
+
$this->nodeData = [];
}
public function process() {
- $quoteNode = WCF::getDIContainer()->get(QuoteHtmlOutputNode::class);
+ // TODO: this should be dynamic to some extent
+ $quoteNode = WCF::getDIContainer()->get(HtmlOutputNodeBlockquote::class);
$quoteNode->process($this);
- $html = $this->document->saveHTML();
-
- // remove nuisance added by PHP
- $html = preg_replace('~^<!DOCTYPE[^>]+>\s<html><body>~', '', $html);
- $html = preg_replace('~</body></html>$~', '', $html);
+ $woltlabMentionNode = WCF::getDIContainer()->get(HtmlOutputNodeWoltlabMention::class);
+ $woltlabMentionNode->process($this);
+ }
+
+ public function getHtml() {
+ $html = parent::getHtml();
/** @var IHtmlOutputNode $obj */
foreach ($this->nodeData as $data) {
return $html;
}
- public function getDocument() {
- return $this->document;
- }
-
public function addNodeData(IHtmlOutputNode $htmlOutputNode, $nodeIdentifier, array $data) {
$this->nodeData[] = [
'data' => $data,
'object' => $htmlOutputNode
];
}
-
- public function renameTag(\DOMElement $element, $tagName) {
- $newElement = $this->document->createElement($tagName);
- $element->parentNode->insertBefore($newElement, $element);
- while ($element->hasChildNodes()) {
- $newElement->appendChild($element->firstChild);
- }
-
- $element->parentNode->removeChild($element);
-
- return $newElement;
- }
-
- public function unwrapContent(\DOMElement $element) {
- while ($element->hasChildNodes()) {
- $element->parentNode->insertBefore($element->firstChild, $element);
- }
-
- $element->parentNode->removeChild($element);
- }
}
<?php
namespace wcf\system\html\output;
-use wcf\system\WCF;
-
class HtmlOutputProcessor {
/**
* @var HtmlOutputNodeProcessor
public function process($html) {
$this->htmlOutputNodeProcessor->load($html);
- $html = $this->htmlOutputNodeProcessor->process();
+ $this->htmlOutputNodeProcessor->process();
- return $html;
+ return $this->htmlOutputNodeProcessor->getHtml();
}
}
--- /dev/null
+<?php
+namespace wcf\system\html\output\node;
+
+use wcf\system\application\ApplicationHandler;
+use wcf\system\html\output\HtmlOutputNodeProcessor;
+use wcf\system\request\RouteHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+class HtmlOutputNodeBlockquote implements IHtmlOutputNode {
+ /**
+ * @var ApplicationHandler
+ */
+ protected $applicationHandler;
+
+ public function __construct(ApplicationHandler $applicationHandler) {
+ $this->applicationHandler = $applicationHandler;
+ }
+
+ public function process(HtmlOutputNodeProcessor $htmlOutputNodeProcessor) {
+ $elements = $htmlOutputNodeProcessor->getDocument()->getElementsByTagName('blockquote');
+ while ($elements->length) {
+ /** @var \DOMElement $blockquote */
+ $blockquote = $elements->item(0);
+
+ if ($blockquote->getAttribute('class') === 'quoteBox') {
+ $nodeIdentifier = StringUtil::getRandomID();
+ $htmlOutputNodeProcessor->addNodeData($this, $nodeIdentifier, [
+ 'title' => ($blockquote->hasAttribute('data-quote-title')) ? $blockquote->getAttribute('data-quote-title') : '',
+ 'url' => ($blockquote->hasAttribute('data-quote-url')) ? $blockquote->getAttribute('data-quote-url') : ''
+ ]);
+
+ $htmlOutputNodeProcessor->renameTag($blockquote, 'wcfNode-' . $nodeIdentifier);
+ }
+ else {
+ $htmlOutputNodeProcessor->unwrapContent($blockquote);
+ }
+ }
+ }
+
+ public function replaceTag(array $data) {
+ $externalQuoteLink = (!empty($data['url'])) ? !$this->applicationHandler->isInternalURL($data['url']) : false;
+ if (!$externalQuoteLink) {
+ $data['url'] = preg_replace('~^https://~', RouteHandler::getProtocol(), $data['url']);
+ }
+
+ $quoteAuthorObject = null;
+ /*
+ * TODO: how should the author object be resolved?
+ *
+ if ($quoteAuthor && !$externalQuoteLink) {
+ $quoteAuthorLC = mb_strtolower(StringUtil::decodeHTML($quoteAuthor));
+ foreach (MessageEmbeddedObjectManager::getInstance()->getObjects('com.woltlab.wcf.quote') as $user) {
+ if (mb_strtolower($user->username) == $quoteAuthorLC) {
+ $quoteAuthorObject = $user;
+ break;
+ }
+ }
+ }
+ */
+
+ WCF::getTPL()->assign([
+ 'quoteLink' => $data['url'],
+ 'quoteAuthor' => $data['title'],
+ 'quoteAuthorObject' => $quoteAuthorObject,
+ 'isExternalQuoteLink' => $externalQuoteLink
+ ]);
+ return WCF::getTPL()->fetch('quoteMetaCode');
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\system\html\output\node;
+
+use wcf\data\user\UserProfile;
+use wcf\data\user\UserProfileCache;
+use wcf\system\application\ApplicationHandler;
+use wcf\system\html\output\HtmlOutputNodeProcessor;
+use wcf\system\request\RouteHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+class HtmlOutputNodeWoltlabMention implements IHtmlOutputNode {
+ /**
+ * @var ApplicationHandler
+ */
+ protected $applicationHandler;
+
+ /**
+ * @var UserProfile[]
+ */
+ protected $userProfiles;
+
+ /**
+ * @var UserProfileCache
+ */
+ protected $userProfileCache;
+
+ public function __construct(ApplicationHandler $applicationHandler, UserProfileCache $userProfileCache) {
+ $this->applicationHandler = $applicationHandler;
+ $this->userProfileCache = $userProfileCache;
+ }
+
+ public function process(HtmlOutputNodeProcessor $htmlOutputNodeProcessor) {
+ $this->userProfiles = [];
+
+ $userIds = [];
+ $elements = $htmlOutputNodeProcessor->getDocument()->getElementsByTagName('woltlab-mention');
+ while ($elements->length) {
+ /** @var \DOMElement $mention */
+ $mention = $elements->item(0);
+
+ $userId = ($mention->hasAttribute('data-user-id')) ? intval($mention->getAttribute('data-user-id')) : 0;
+ $username = ($mention->hasAttribute('data-username')) ? StringUtil::trim($mention->getAttribute('data-username')) : '';
+
+ if ($userId === 0 || $username === '') {
+ $mention->parentNode->removeChild($mention);
+ continue;
+ }
+
+ $userIds[] = $userId;
+ $nodeIdentifier = StringUtil::getRandomID();
+ $htmlOutputNodeProcessor->addNodeData($this, $nodeIdentifier, [
+ 'userId' => $userId,
+ 'username' => $username
+ ]);
+
+ $htmlOutputNodeProcessor->renameTag($mention, 'wcfNode-' . $nodeIdentifier);
+ }
+
+ if (!empty($userIds)) {
+ $this->userProfiles = $this->userProfileCache->getUserProfiles($userIds);
+ }
+ }
+
+ public function replaceTag(array $data) {
+ WCF::getTPL()->assign([
+ 'username' => $data['username'],
+ 'userId' => $data['userId'],
+ 'userProfile' => $this->userProfiles[$data['userId']]
+ ]);
+
+ return WCF::getTPL()->fetch('htmlNodeWoltlabMention');
+ }
+}
\ No newline at end of file
+++ /dev/null
-<?php
-namespace wcf\system\html\output\node;
-
-use wcf\system\application\ApplicationHandler;
-use wcf\system\html\output\HtmlOutputNodeProcessor;
-use wcf\system\request\RouteHandler;
-use wcf\system\WCF;
-use wcf\util\StringUtil;
-
-class QuoteHtmlOutputNode implements IHtmlOutputNode {
- /**
- * @var ApplicationHandler
- */
- protected $applicationHandler;
-
- public function __construct(ApplicationHandler $applicationHandler) {
- $this->applicationHandler = $applicationHandler;
- }
-
- public function process(HtmlOutputNodeProcessor $htmlOutputNodeProcessor) {
- $elements = $htmlOutputNodeProcessor->getDocument()->getElementsByTagName('blockquote');
- while ($elements->length) {
- /** @var \DOMElement $blockquote */
- $blockquote = $elements->item(0);
-
- if ($blockquote->getAttribute('class') === 'quoteBox') {
- $nodeIdentifier = StringUtil::getRandomID();
- $htmlOutputNodeProcessor->addNodeData($this, $nodeIdentifier, [
- 'title' => ($blockquote->hasAttribute('data-quote-title')) ? $blockquote->getAttribute('data-quote-title') : '',
- 'url' => ($blockquote->hasAttribute('data-quote-url')) ? $blockquote->getAttribute('data-quote-url') : ''
- ]);
-
- $htmlOutputNodeProcessor->renameTag($blockquote, 'wcfNode-' . $nodeIdentifier);
- }
- else {
- $htmlOutputNodeProcessor->unwrapContent($blockquote);
- }
- }
- }
-
- public function replaceTag(array $data) {
- $externalQuoteLink = (!empty($data['url'])) ? !$this->applicationHandler->isInternalURL($data['url']) : false;
- if (!$externalQuoteLink) {
- $data['url'] = preg_replace('~^https://~', RouteHandler::getProtocol(), $data['url']);
- }
-
- $quoteAuthorObject = null;
- /*
- * TODO: how should the author object be resolved?
- *
- if ($quoteAuthor && !$externalQuoteLink) {
- $quoteAuthorLC = mb_strtolower(StringUtil::decodeHTML($quoteAuthor));
- foreach (MessageEmbeddedObjectManager::getInstance()->getObjects('com.woltlab.wcf.quote') as $user) {
- if (mb_strtolower($user->username) == $quoteAuthorLC) {
- $quoteAuthorObject = $user;
- break;
- }
- }
- }
- */
-
- WCF::getTPL()->assign(array(
- 'quoteLink' => $data['url'],
- 'quoteAuthor' => $data['title'],
- 'quoteAuthorObject' => $quoteAuthorObject,
- 'isExternalQuoteLink' => $externalQuoteLink
- ));
- return WCF::getTPL()->fetch('quoteMetaCode');
- }
-}
\ No newline at end of file
'requestMethod' => $this->requestMethod,
'lastActivityTime' => TIME_NOW
);
- if (!class_exists('wcf\system\CLIWCF', false) && PACKAGE_ID && RequestHandler::getInstance()->getActiveRequest() && RequestHandler::getInstance()->getActiveRequest()->getRequestObject() instanceof ITrackablePage && RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->isTracked()) {
+ /*if (!class_exists('wcf\system\CLIWCF', false) && PACKAGE_ID && RequestHandler::getInstance()->getActiveRequest() && RequestHandler::getInstance()->getActiveRequest()->getRequestObject() instanceof ITrackablePage && RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->isTracked()) {
$data['controller'] = RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->getController();
$data['parentObjectType'] = RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->getParentObjectType();
$data['parentObjectID'] = RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->getParentObjectID();
$data['objectType'] = RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->getObjectType();
$data['objectID'] = RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->getObjectID();
- }
+ }*/
if ($this->variablesChanged) {
$data['sessionVariables'] = serialize($this->variables);
}